Zum Hauptinhalt springen
Version: Aktuellste Version

Nacht­schicht (12-12-Modell) – Entwicklerdokumentation

Diese Dokumentation beschreibt das Feature Nacht­schicht der biz.Suite. Technisch handelt es sich nicht um ein eigenes Entity oder einen eigenen Schalter „Nachtschicht", sondern um das Berechnungsperioden-Modell 12_12, das als Eigenschaft calculationPeriod am Entity TimeProfile hängt. Ist dieses Modell aktiv, verschiebt sich der abzurechnende Tag von 00:00–24:00 (Standard) auf 12:00–12:00 des Folgetags, sodass eine über Mitternacht gehende Nachtschicht in einem Arbeitszeitsjournal zusammengefasst wird.

Sie richtet sich an Backend- und Frontend-Entwickler, die das Feature pflegen, erweitern oder integrieren.

Stand: 2026-07-10 Repositorys:

  • Backend: biz.Suite_Backend (Symfony / Doctrine / PHP)
  • Frontend: biz.Suite_Frontend (Angular / TypeScript)

Verwandte Dokumentation: Arbeitszeitsjournale – Entwicklerdokumentation, Arbeitszeitsjournale.md.


Inhaltsverzeichnis​

  1. Übersicht & Motivation
  2. Kernkonzept: die Berechnungsperiode
  3. Datenmodell
  4. Die zentrale Verschiebe-Logik (getAdjustedDate)
  5. Backend-Komponenten im Detail
  6. Frontend-Komponenten im Detail
  7. Ende-zu-Ende-Ablauf einer Nachtschicht
  8. Durchgerechnetes Beispiel
  9. Migration & Einführung
  10. Fallstricke, Grenzen & offene Punkte
  11. Referenz: alle Fundstellen

1. Übersicht & Motivation​

Im Standardfall (0_24) wird ein Arbeitszeitsjournal für den Kalendertag von 00:00 bis 24:00 gerechnet. Für Nachtschichten ist das ungünstig: Eine Schicht von z. B. 22:00 bis 06:00 würde auf zwei Kalendertage – und damit auf zwei Journaleinträge – aufgeteilt. Weder Soll-Zeit (Profilzeit) noch Ist-Zeit (Stempelungen) noch der Saldo lassen sich dann sinnvoll einer einzelnen Schicht zuordnen.

Das Nachtschicht-Feature löst das über das Berechnungsmodell 12_12: Der abzurechnende Zeitraum eines Journals mit Datum T läuft von T-1 12:00 bis T 12:00. Damit liegt eine komplette Nachtschicht innerhalb einer Periode und wird in einem Journal abgerechnet.

Die Umsetzung besteht im Kern aus zwei Bausteinen:

  1. Bestimmung des Journal-Zeitfensters – die Zeitgrenzen, in denen Stempelungen und Abwesenheiten für ein Journal gesucht werden.
  2. Verschiebung der Tagesblock-Zeiten (TimeProfileDayLine) – Uhrzeiten ≥ 12:00 werden logisch dem Vortag zugeordnet, damit die Overlap-Berechnung zwischen Soll-Blöcken und Ist-Stempelungen über Mitternacht hinweg korrekt funktioniert.

Namenshinweis: Im Code existiert der Begriff „Nachtschicht"/„Night-shift" nicht wörtlich. Gesucht/gepflegt wird das Feature ausschließlich über die Werte calculationPeriod bzw. calculation_period und die String-Konstante '12_12'.


2. Kernkonzept: die Berechnungsperiode​

Die Eigenschaft calculationPeriod kennt genau zwei gültige Werte:

WertBedeutungJournal-Zeitfenster für Datum T
0_24Standard-Tagesabrechnung (Default)T 00:00:00 – T 23:59:59
12_12NachtschichtT-1 12:00:00 – T 12:00:00
nullAlt-Datenbestand vor Einführung des Feldswird wie 0_24 behandelt (siehe Kap. 9)

Die entscheidende Grenzzeit ist überall fest auf 12:00 Uhr verdrahtet (->setHours(12), setTime(12, 0, 0, 0) bzw. im Frontend setHours(12, 0, 0, 0)). Es gibt keine Konfigurationsmöglichkeit, diese Grenze zu verschieben – „12_12" ist wörtlich „ab 12 Uhr bis 12 Uhr".

Die Grundregel für die Zeitverschiebung lautet in allen Umsetzungen identisch:

Eine Uhrzeit ≥ 12:00 gehört zum Vortag der Periode; eine Uhrzeit < 12:00 gehört zum Journaltag selbst.


3. Datenmodell​

3.1 Entity TimeProfile​

Datei: biz.Suite_Backend/src/Entity/TimeProfile.php

// Can be '0_24' or '12_12'
#[ORM\Column(length: 255, nullable: true)]
private ?string $calculationPeriod = null;

public function getCalculationPeriod(): ?string
{
return $this->calculationPeriod;
}

public function setCalculationPeriod(string $calculationPeriod): static
{
$this->calculationPeriod = $calculationPeriod;
return $this;
}

Merkmale:

  • Nullable VARCHAR(255). Es gibt keinen DB-seitigen Enum-Constraint und keine serverseitige Validierung, die die Werte auf 0_24/12_12 einschränkt. Die Gültigkeit wird faktisch nur durch das Frontend-Dropdown (Kap. 6.1) sichergestellt.
  • Der Getter ist nullable (?string), der Setter erwartet dagegen einen nicht-nullbaren string. Ein explizites Zurücksetzen auf null über die Domäne ist damit nicht vorgesehen.
  • Das Feld wird per JMS-Serializer standardmäßig mit ausgeliefert (keine #[Serializer\Exclude]-Annotation) und erscheint im JSON als calculation_period.

3.2 Datenbank-Migration​

Datei: biz.Suite_Backend/migrations/Version20260422143051.php

public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE time_profile ADD calculation_period VARCHAR(255) DEFAULT NULL');
}

public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE time_profile DROP calculation_period');
}

Die Spalte wird ohne DB-Default (bzw. DEFAULT NULL) angelegt. Bestehende TimeProfile-Datensätze haben nach der Migration also calculation_period = NULL und müssen per Console-Command nachgezogen werden (siehe Kap. 9).

3.3 Beziehung zu den übrigen Time-Profile-Entities​

Das Feld hängt am TimeProfile, wirkt sich aber auf die abgeleiteten Entities aus:

  • TimeProfileDay – Wochentag-Zuordnung; enthält die TimeProfileDayLines.
  • TimeProfileDayLine – einzelner Soll-Zeitblock mit startTime/endTime. Diese Uhrzeiten werden im 12_12-Modell verschoben.
  • TimeProfileWorkerJournal – das Tagesjournal, dessen Zeitfenster im 12_12-Modell verschoben wird.
  • TimeControlBooking – die realen Stempelungen, die innerhalb des verschobenen Fensters gesucht werden.

4. Die zentrale Verschiebe-Logik (getAdjustedDate)​

Das Herzstück der Nachtschicht ist die Hilfsmethode getAdjustedDate(). Sie existiert inhaltlich identisch an drei Stellen im Backend (und einmal analog im Frontend). Sie nimmt eine reine Uhrzeit (aus einer TimeProfileDayLine) und das Journal-Datum und liefert einen konkreten DateTime:

private function getAdjustedDate(?DateTime $time, ?DateTime $date, TimeProfile $timeProfile): ?DateTime
{
if (!$time || !$date) {
return null;
}

$carbonDate = new Carbon($time);

// Uhrzeit aus $time auf das Journal-Datum setzen
$targetDateTime = \DateTime::createFromInterface($date)->setTime(
$carbonDate->hour,
$carbonDate->minute,
$carbonDate->second
);

$calculationPeriodEnd = (new Carbon($date))->setTime(12, 0, 0, 0);

// 12_12: Uhrzeiten ab 12:00 gehören zum Vortag
if ($timeProfile->getCalculationPeriod() === '12_12' && $targetDateTime >= $calculationPeriodEnd) {
$targetDateTime = (new Carbon($targetDateTime))->subDay();
}

return $targetDateTime;
}

Wirkung: Bei 0_24 bleibt jede Uhrzeit am Journaltag. Bei 12_12 wird eine Uhrzeit ≥ 12:00 um einen Tag nach hinten (-1 Tag) geschoben, während Uhrzeiten < 12:00 am Journaltag verbleiben. Dadurch entsteht aus den Soll-Blöcken einer Nachtschicht eine chronologisch zusammenhängende Kette über Mitternacht:

Beispiel-Tagesblöcke einer 22:00–06:00-Schicht, Journal-Datum = T
22:00 -> T-1 22:00 (>= 12:00 -> Vortag)
23:00 -> T-1 23:00
00:00 -> T 00:00 (< 12:00 -> Journaltag)
06:00 -> T 06:00

⚠️ Wichtig – Code-Duplizierung: Die Methode ist dreimal dupliziert (siehe Kap. 5). Änderungen an der Verschiebe-Logik müssen an allen Stellen synchron erfolgen. Eine kleine Abweichung existiert bereits: In TimeProfileBookingErrorProcessor fehlt der einleitende if (!$time || !$date)-Null-Guard.


5. Backend-Komponenten im Detail​

Alle relevanten Klassen liegen unter biz.Suite_Backend/src/Processor/TimeProfiles/ bzw. im Controller.

5.1 TimeProfileWorkerJournalCalculationProcessor​

Datei: .../Processor/TimeProfiles/TimeProfileWorkerJournalCalculationProcessor.php

Zentrale Klasse, die ein Journal berechnet. Zwei Stellen betreffen die Nachtschicht:

a) Bestimmung des Buchungs-Zeitfensters (calculateJournalTimes, ~Z. 63):

if ($journal->getTimeProfile()->getCalculationPeriod() === '12_12') {
$start = (new Carbon($journal->getDate()))->subDay()->setHours(12);
$end = (new Carbon($journal->getDate()))->setHours(12);
} else {
// Default to the standard 0-24 time period
$start = (new Carbon($journal->getDate()))->startOfDay();
$end = (new Carbon($journal->getDate()))->endOfDay();
}

$bookings = $timeControlBookingRepository->findInRangeFiltered($start, $end, $journal->getWorker(), null, true);

Hinweis zur Grenzzeit: ->setHours(12) setzt nur die Stunde und lässt Minuten/Sekunden unverändert. Da das Journaldatum in aller Regel um 00:00 beginnt, ergibt sich effektiv 12:00:00. Bei einem Journal-date mit abweichender Uhrzeit wären $start/$end entsprechend verschoben – ein potenzieller Stolperstein.

b) getAdjustedDate (private, ~Z. 489) – wird von allen Berechnungs-Unterschritten verwendet, u. a.:

  • getDayAndDayLines() – Sortierung der Tagesblöcke nach ihrem verschobenen Startzeitpunkt (damit die Blöcke über Mitternacht chronologisch stimmen).
  • getOverlappingBookingEntries() – Ermittlung, welche Stempel-Intervalle einen Soll-Block überlappen.
  • calculatePaidTime(), calculateTotalBreakTime(), consumeUnplannedBreakTimePresence(), consumeToleranceTime() – alle rechnen mit den verschobenen Block-Grenzen ($dayLineStart/$dayLineEnd).
  • isWithinTolerance() – Toleranzfenster vor/nach Blockgrenzen.

Damit zieht sich die Verschiebung transparent durch die gesamte Zeit-, Pausen-, Überstunden- und Toleranzberechnung. Wichtig: Die Stempelungen selbst (TimeControlBooking.entryDateTime) sind bereits echte Zeitpunkte und werden nicht verschoben – nur die aus reinen Uhrzeiten bestehenden Soll-Blöcke werden auf konkrete Zeitpunkte projiziert.

5.2 ProfileTimeCalculationProcessor​

Datei: .../Processor/TimeProfiles/ProfileTimeCalculationProcessor.php

Berechnet die Soll-/Profilzeit eines Tages (in Sekunden). Hier wird nicht getAdjustedDate verwendet, sondern eine eigene, gleichwertige Sonderbehandlung (calculateProfileTime, ~Z. 45):

$endTime   = (new Carbon($dayLine->getEndTime()))->setDateFrom(new Carbon());
$startTime = (new Carbon($dayLine->getStartTime()))->setDateFrom(new Carbon());

$calculationPeriodEnd = (new Carbon())->setTime(12, 0, 0, 0);

// Start liegt nach 12:00, Ende vor 12:00 -> Block läuft über zwei Tage
// => Start um einen Tag zurücksetzen, damit die Dauer positiv bleibt
if (
$timeProfileDay->getTimeProfile()?->getCalculationPeriod() === '12_12'
&& ($startTime >= $calculationPeriodEnd && $endTime < $calculationPeriodEnd)
) {
$startTime = (clone $startTime)->modify('-1 day');
}

$profileTime += ($endTime->getTimestamp() ?? 0) - ($startTime->getTimestamp() ?? 0);

Nur Blöcke, deren Typ isCalcStandardTime() liefert, zählen in die Profilzeit. Die Sonderregel greift genau für den einen Block, der über Mitternacht läuft (Start ≥ 12:00, Ende < 12:00), und verhindert eine negative Dauer.

⚠️ Diese Klasse bezieht ihr Datum aus new Carbon() (heute), nicht aus dem Journaldatum – für die reine Dauer eines Blocks ist das unerheblich, es ist aber eine bewusst andere Implementierung als getAdjustedDate.

5.3 TimeProfileBookingErrorProcessor​

Datei: .../Processor/TimeProfiles/TimeProfileBookingErrorProcessor.php

Prüft Stempelungen auf Fehler (falsche Reihenfolge, außerhalb gültiger Blöcke). Enthält eine eigene Kopie von getAdjustedDate (~Z. 178, ohne Null-Guard), verwendet in findMatchingDayLine(). Dadurch wird bei einer Nachtschicht korrekt geprüft, ob eine Stempelung nach Mitternacht noch in einen (verschobenen) Soll-Block inkl. Toleranz fällt.

5.4 TimeProfileWorkerJournalControllerV2​

Datei: .../Controller/TimeProfileWorkerJournalControllerV2.php (~Z. 454)

Der Endpoint GET /api/v2/timeProfileWorkerJournals/events/{id} (getBookingsByJournal) liefert Stempelungen und Abwesenheiten eines Journals für die UI. Er verwendet dasselbe Fenster-Schema:

if ($journal->getTimeProfile()?->getCalculationPeriod() === '12_12') {
$start = (new Carbon($journal->getDate()))->subDay()->setHours(12);
$end = (new Carbon($journal->getDate()))->setHours(12);
} else {
$start = (new Carbon($journal->getDate()))->startOfDay();
$end = (new Carbon($journal->getDate()))->endOfDay();
}

$bookings = $bookingRepository->findInRangeFiltered($start, $end, $journal->getWorker());
$absences = $absenceRepository->findByAbsenceCodes(..., $start, $end, ...);

So sieht das Frontend genau die Stempelungen/Abwesenheiten, die auch in die Berechnung eingehen.

5.5 Migrations-Command​

Datei: .../Command/TimeProfileCalculationPeriodCommand.php

Command app:migrate-time-profile-calculation-period. Setzt bei allen TimeProfiles eines Mandanten (SysClient) calculationPeriod auf den Standard '0_24' (überspringt den Master-Mandanten). Details siehe Kap. 9.


6. Frontend-Komponenten im Detail​

Alle Pfade relativ zu biz.Suite_Frontend/src/app/main/apps/timecontrol/. Im Frontend heißt das Feld calculation_period (Snake-Case aus dem JSON).

6.1 Arbeitszeitmodell-Dialog – Auswahl des Modells​

Datei: time-profiles/time-profile-dialogs/time-profile-dialog/time-profile-dialog.component.html (~Z. 131)

Hier wird das Modell gesetzt – das ist der einzige Ort, an dem ein Anwender die Nachtschicht aktiviert:

<mat-form-field appearance="outline" fxFlex="200">
<mat-label>Berechnung</mat-label>
<mat-select placeholder="Keine Ahnung" formControlName="calculation_period" required>
<mat-option value="0_24">0-24 Modell</mat-option>
<mat-option value="12_12">12-12 Modell</mat-option>
</mat-select>
</mat-form-field>

Das Feld ist required; über das formControlName="calculation_period" wird der Wert beim Speichern des TimeProfile übertragen. (Die Labels „Berechnung"/„Keine Ahnung" sind hartkodiert und nicht übersetzt – siehe offene Punkte.)

6.2 TimeProfileDayLine.model.ts – Verschiebung der Block-Startzeit​

Datei: time-profiles/TimeProfileDayLine.model.ts (~Z. 42)

Analog zum Backend-getAdjustedDate verschiebt das Frontend-Modell die Startzeit eines Blocks für die Anzeige/Sortierung:

if (this.time_profile_day.time_profile.calculation_period === '12_12') {
const now = new Date();
const [hours, minutes] = timeProfileDayLine.start_time.split(':');

const targetTime = new Date(
new Date(new Date().setFullYear(now.getFullYear(), now.getMonth(), now.getDate()))
.setHours(hours, minutes, 0)
);

this.start_time = new Date(this.start_time) >= new Date(new Date().setHours(12, 0, 0, 0))
? targetTime.toString()
: this.start_time;
}

Startzeiten ≥ 12:00 werden auf ein volles Datum umgerechnet, damit die nachfolgende Sortierung über Mitternacht funktioniert.

6.3 time-profile-day.datasource.ts – Sortierung der Blöcke​

Datei: time-profiles/time-profile-details/time-profile-days/time-profile-day.datasource.ts (~Z. 45)

.sort((a, b) => {
if (a.time_profile_day.time_profile.calculation_period === '12_12') {
return new Date(a.start_time).getTime() - new Date(b.start_time).getTime();
} else {
return a.start_time_minutes - b.start_time_minutes;
}
});

Im 12_12-Modell wird nach dem (in 6.2 gesetzten) vollständigen Datum sortiert, im Standardmodell nach reinen Minuten-im-Tag. So erscheinen die Blöcke einer Nachtschicht in korrekter chronologischer Reihenfolge (z. B. 22:00 → 23:00 → 00:00 → 06:00).

6.4 Journal-Detailansicht – Datumsanzeige​

Dateien:

  • time-profile-worker-v2/.../time-profile-worker-details-v2.component.ts (~Z. 42, 76)
  • .../time-profile-worker-details-v2.component.html (~Z. 155)
calculationPeriod: string = '0_24';
yesterday: Date | null = null;
// ...
this.calculationPeriod = journal.time_profile?.calculation_period ?? '0_24';
this.yesterday = journal.date
? new Date(new Date().setDate(new Date(journal.date).getDate() - 1))
: null;
<div *ngIf="calculationPeriod === '12_12'; else elseBlock" class="flex">
<span>{{ (yesterday | date:'mediumDate') ?? 'Keine Daten' }}</span>
<span>&nbsp;/&nbsp;</span>
<span>{{ (selectedJournal?.date | date:'mediumDate') ?? 'Keine Daten' }}</span>
</div>
<ng-template #elseBlock>
<span>{{ (selectedJournal?.date | date:'mediumDate') ?? 'Keine Daten' }}</span>
</ng-template>

Im Nachtschicht-Modus zeigt die Detailansicht beide Tage der Periode an (Vortag / Journaltag, z. B. 09.07.2026 / 10.07.2026), im Standardmodus nur den einen Kalendertag. Fallback bei fehlendem Wert ist '0_24'.

6.5 Buchungs-Dialog – manuelles Anlegen/Bearbeiten einer Stempelung​

Datei: time-profile-worker-v2/.../time-profile-worker-booking-dialog-v2.component.ts (~Z. 81)

Beim manuellen Erfassen einer Stempelung muss die eingegebene Uhrzeit dem richtigen Kalendertag zugeordnet werden:

const [hours, minutes] = rawValue.startTime.split(':');
const targetTime = new Date(new Date().setHours(hours, minutes, 0, 0));
const calculationPeriodEnd = new Date(new Date().setHours(12, 0, 0, 0));

const targetDate = this.data.journal.time_profile?.calculation_period === '12_12' && (targetTime >= calculationPeriodEnd)
? new Date(new Date().setDate(new Date(this.data?.journal?.date).getDate() - 1))
: new Date(this.data?.journal?.date);

const dateTime = `${formatDate(targetDate, 'yyyy-MM-dd', 'en-US')} ${rawValue.startTime}:00`;

Wird also in einem 12_12-Journal eine Stempelung mit Uhrzeit ≥ 12:00 angelegt, landet sie auf dem Vortag – konsistent zur Backend-Fensterlogik –, Uhrzeiten < 12:00 auf dem Journaltag. So fällt die manuell angelegte Buchung in dasselbe Fenster, in dem das Backend sie sucht.


7. Ende-zu-Ende-Ablauf einer Nachtschicht​

┌──────────────────────────────────────────────────────────────────────┐
│ 1. Admin legt TimeProfile an, wählt im Dialog "12-12 Modell" │
│ -> POST/PUT TimeProfile { calculation_period: "12_12" } │
│ (time-profile-dialog.component.html) │
└──────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ 2. Tagesblöcke (TimeProfileDayLine) werden mit Uhrzeiten definiert, │
│ z. B. 22:00–06:00. FE verschiebt Startzeiten >= 12:00 zur Anzeige │
│ (TimeProfileDayLine.model.ts, day.datasource.ts) │
└──────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ 3. Mitarbeiter stempelt real (Check-In 21:58 ... Check-Out 06:03). │
│ TimeControlBookings mit echten entry_date_time. │
└──────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ 4. Journal-Berechnung (TimeProfileWorkerJournalCalculationProcessor) │
│ a) Fenster: T-1 12:00 .. T 12:00 │
│ b) Soll-Blöcke via getAdjustedDate auf Zeitpunkte projizieren │
│ c) Overlap Ist/Soll, Pausen, Toleranz, Überstunden, Saldo │
│ d) Fehlerprüfung (TimeProfileBookingErrorProcessor) │
└──────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ 5. Anzeige im Journal-Detail: "Vortag / Journaltag", Events-Endpoint │
│ liefert Buchungen aus demselben 12:00–12:00-Fenster │
│ (details-v2 component, ControllerV2::getBookingsByJournal) │
└──────────────────────────────────────────────────────────────────────┘

8. Durchgerechnetes Beispiel​

Setup: TimeProfile mit calculationPeriod = '12_12'. Journal-Datum T = 2026-07-10 (Freitag). Ein Soll-Block (Standardzeit) 22:00–06:00.

Fenster (Schritt 4a): start = 2026-07-09 12:00, end = 2026-07-10 12:00.

Soll-Block-Projektion (getAdjustedDate, Schritt 4b):

Block-UhrzeitVergleich mit 12:00Ergebnis-Zeitpunkt
startTime = 22:00≥ 12:00 → Vortag2026-07-09 22:00
endTime = 06:00< 12:00 → Journaltag2026-07-10 06:00

→ Soll-Intervall 2026-07-09 22:00 … 2026-07-10 06:00, Dauer 8 h = Profilzeit (ProfileTimeCalculationProcessor, Über-Mitternacht-Sonderregel greift, da Start ≥ 12:00 und Ende < 12:00).

Ist-Stempelungen (im Fenster gefunden): Check-In 2026-07-09 21:58, Check-Out 2026-07-10 06:03. Beide liegen im Fenster 09.07. 12:00 … 10.07. 12:00 und überlappen den projizierten Soll-Block → bezahlte Arbeitszeit ≈ 8 h (abzüglich Pausen/Toleranz gem. weiterer Logik). Saldo wird gegen die 8 h Profilzeit gerechnet.

Anzeige: Detailkopf zeigt 09.07.2026 / 10.07.2026.

Zum Vergleich 0_24: Dieselben Stempelungen fielen in zwei Journale (09.07. und 10.07.), keines enthielte die volle Schicht.


9. Migration & Einführung​

9.1 Reihenfolge​

  1. Schema-Migration Version20260422143051.php ausführen → Spalte calculation_period (NULL) wird angelegt.

  2. Daten-Migration über den Command:

    php bin/console app:migrate-time-profile-calculation-period

    Setzt bei jedem bestehenden TimeProfile calculationPeriod = '0_24' (pro Mandant; der Master-SysClient wird übersprungen). Damit erhalten Altprofile explizit das Standardmodell.

9.2 Verhalten bei null​

Solange die Daten-Migration nicht gelaufen ist, ist calculationPeriod = null. Alle Vergleiche im Code lauten strikt === '12_12' und schlagen bei null fehl → das System verhält sich automatisch wie 0_24. Ein null-Profil ist damit funktional sicher, aber im FE-Dialog erscheint kein vorausgewähltes Modell (Feld ist required).


10. Fallstricke, Grenzen & offene Punkte​

ThemaBeschreibungOrt
Dreifache Code-DuplizierunggetAdjustedDate existiert 3× im Backend; Änderungen müssen synchron erfolgen. ProfileTimeCalculationProcessor nutzt zudem eine 4., eigenständige Variante.Processor-Klassen
Fehlender Null-GuardDie Kopie in TimeProfileBookingErrorProcessor::getAdjustedDate prüft $time/$date nicht auf null (anders als die beiden anderen Kopien).TimeProfileBookingErrorProcessor.php:178
Harte 12:00-GrenzeDie Periodengrenze ist überall fest 12:00 Uhr. Keine Konfiguration möglich; ein „14-14-Modell" o. ä. ist nicht vorgesehen.alle Fundstellen
->setHours(12) statt setTime(12,0,0,0)Im Journal-Fenster und im Controller wird nur die Stunde gesetzt. Bei einem Journal-date mit Nicht-Null-Uhrzeit verschöbe sich das Fenster. Funktioniert nur, weil date i. d. R. 00:00 ist.Calculation­Processor Z. 63 ff., ControllerV2 Z. 454 ff.
Keine Wert-ValidierungWeder DB noch Backend beschränken calculationPeriod auf 0_24/12_12. Gültigkeit hängt allein am FE-Dropdown.TimeProfile.php
Setter nicht nullbarsetCalculationPeriod(string ...) erlaubt kein Zurücksetzen auf null über die Domäne, obwohl das Feld/der Getter nullbar sind.TimeProfile.php:221
FE nutzt „heute" als BasisTimeProfileDayLine.model.ts und der Buchungs-Dialog konstruieren Zeitpunkte teils aus new Date() (heute) statt aus dem Journaldatum. Für Anzeige/Sortierung unkritisch, aber semantisch fragil.FE-Dateien Kap. 6
Keine TestsFür die Nachtschicht-/12_12-Logik existieren keine automatisierten Tests (biz.Suite_Backend/tests enthält keine Fundstellen).–
Hartkodierte LabelsDropdown-Label „Berechnung" und Platzhalter „Keine Ahnung" sind nicht übersetzt.time-profile-dialog HTML

11. Referenz: alle Fundstellen​

Backend​

DateiRolleSchlüsselstelle
Entity/TimeProfile.phpFeld-Definition calculationPeriod, Getter/SetterZ. 61–63, 216–226
migrations/Version20260422143051.phpSchema-Migration calculation_periodZ. 23, 29
Command/TimeProfileCalculationPeriodCommand.phpDaten-Migration auf '0_24'Z. 50
Processor/TimeProfiles/TimeProfileWorkerJournalCalculationProcessor.phpFenster + getAdjustedDate in gesamter BerechnungZ. 63, 489–510
Processor/TimeProfiles/ProfileTimeCalculationProcessor.phpProfil-/Sollzeit, Über-Mitternacht-SonderregelZ. 45–50
Processor/TimeProfiles/TimeProfileBookingErrorProcessor.phpFehlerprüfung, eigene getAdjustedDate-KopieZ. 178–195
Controller/TimeProfileWorkerJournalControllerV2.phpEvents-Endpoint, FensterlogikZ. 454

Frontend​

DateiRolleSchlüsselstelle
time-profiles/.../time-profile-dialog.component.htmlModell-Auswahl 0_24/12_12Z. 131–143
time-profiles/TimeProfileDayLine.model.tsVerschiebung Block-StartzeitZ. 42–51
time-profiles/.../time-profile-day.datasource.tsSortierung der BlöckeZ. 45–51
.../time-profile-worker-details-v2.component.tscalculationPeriod, yesterdayZ. 42, 76–77
.../time-profile-worker-details-v2.component.htmlDatumsanzeige „Vortag / Tag"Z. 155–169
.../time-profile-worker-booking-dialog-v2.component.tsManuelle Buchung auf richtigen Tag legenZ. 81–89

Ende der Dokumentation.