Arbeitszeitsjournale – Entwicklerdokumentation
Diese Dokumentation beschreibt das Feature Arbeitszeitsjournale (engl. TimeProfileWorkerJournals) der biz.Suite. Sie richtet sich an Backend- und Frontend-Entwickler, die das Feature pflegen, erweitern oder integrieren.
Stand: 2026-04-30 Repositorys:
- Backend:
biz.Suite_Backend(Symfony / Doctrine) - Frontend:
biz.Suite_Frontend(Angular)
Inhaltsverzeichnis
- Übersicht
- Begriffe & Domänenmodell
- Architektur
- Datenmodell (Entities)
- TimeControlBooking – Stempelbuchungen
- Backend-Komponenten
- Frontend-Komponenten
- Konfiguration & alle FE-Einstellungen / Schalter
- Berechnungslogik im Detail
- Lifecycle eines Journals
- API-Endpunkte
- Berechtigungen
- Reports & Dashboards
- Console Commands & geplante Jobs
1. Übersicht
Das Arbeitszeitsjournal ist die zentrale Tagesabrechnung der Arbeitszeit eines Mitarbeiters. Pro Mitarbeiter und Kalendertag existiert genau ein Journaleintrag, der
- die Soll-Arbeitszeit (Profilzeit) gemäß zugewiesenem Arbeitszeitmodell und Wochentag,
- die Ist-Arbeitszeit aus den
TimeControlBooking-Stempelungen (Check-In, Pause, Check-Out), - bezahlte und unbezahlte Abwesenheiten,
- Überstunden,
- den Tagessaldo sowie den akkumulierten Saldo und
- den Bearbeitungsstatus (gültig/ungültig, berechnet, genehmigt, umgebucht)
konsolidiert. Die Berechnung erfolgt serverseitig über die Processor-Klassen unter biz.Suite_Backend/src/Processor/TimeProfiles/. Die UI wird in der Frontend-App unter biz.Suite_Frontend/src/app/main/apps/timecontrol/ bereitgestellt – derzeit existieren V1- und V2-Komponenten parallel; V2 ist die aktive Implementierung.
2. Begriffe & Domänenmodell
| Deutsch | Englisch / Klassenname | Bedeutung |
|---|---|---|
| Arbeitszeitmodell | TimeProfile | Vorlage für Arbeitszeiten (z. B. „8h Standard", „6h Gleitzeit") |
| Tag (im Modell) | TimeProfileDay | Zuordnung eines Modells zu einem Wochentag (Mo–So) |
| Tagesblock | TimeProfileDayLine | Einzelner Zeitblock innerhalb eines Tages mit Start/Ende und Toleranzen |
| Zeitprofiltyp | TimeProfileType | Klassifikation eines Tagesblocks (Standardzeit, Überstunden, Pause, Gleitzeit, …) – steuert die Berechnung |
| Schichtplan / Zeitmodellgruppe | TimeProfileGroup | Gruppe von Mitarbeitern, die ein gemeinsames Arbeitszeitmodell teilen |
| Zeitmodellkalender | TimeProfileCalendar | Tagesgenauer Kalendereintrag, der einem Mitarbeiter ein konkretes TimeProfile zuweist |
| Pausen-Einstellung | TimeProfileBreakSetting | Schwellenwert-basierte Pflichtpausenregel pro TimeProfile |
| Stempelung | TimeControlBooking | Einzelnes Stempel-Ereignis (Check-In/Out, Pause) mit Zeitstempel und ggf. GPS |
| Buchungstyp | TimeControlBookingType | Typ einer Stempelbuchung (Check-In, Check-Out, Pause, …) |
| Buchungsantrag | TimeControlBookingRequest | Antrag auf nachträgliche Stempelung (Workflow mit Genehmigung) |
| Arbeitszeitsjournal | TimeProfileWorkerJournal | Tagesjournal pro Mitarbeiter mit Berechnungsergebnissen |
| Bewertungseintrag | TimeProfileWorkerJournalEvaluation | Detaildatenzeilen pro Journal (Aufteilung nach Standort/Buchungstyp) |
| Saldokorrektur | BalanceCorrection | Manuelle Korrektur des Tagessaldos |
3. Architektur
┌──────────────────────────────────────────────────────────────────┐
│ Frontend (Angular) │
│ │
│ /timecontrol/time-profile-worker-journals (V2) │
│ /timecontrol/main (Stempel-UI / Terminal) │
│ /timecontrol/settings/{time-profiles, time-profile-types, │
│ time-profile-groups, booking-types, │
│ time-chips, worker-chips, basic-settings} │
│ │
└──────────────── ─────┬────────────────────────────────────────────┘
│ REST/JSON (api/, api/v2/)
▼
┌──────────────────────────────────────────────────────────────────┐
│ Backend (Symfony) │
│ │
│ Controller ──▶ Processor ──▶ Repository ──▶ Doctrine ORM ──▶ DB │
│ │ │ │ │
│ │ ▼ ▼ │
│ │ BookingError Elastica Index │
│ │ BalanceCalc (Suche) │
│ │ ProfileTime │
│ │ Notification │
│ │ │
│ └──▶ EventListener (Doctrine preUpdate) ──▶ Notifications │
│ │
│ Console Commands (geplant via Cron): │
│ - app:create-time-profile-work-journals │
│ - app:calculate-time-profile-journals │
│ - app:time-profile-calendar-entry-generation │
│ - app:generate-time-profile-worker-journal-evaluation │
└──────────────────────────────────────────────────────────────────┘
Datenfluss eines typischen Tages:
- Mitarbeiter stempelt → ein neuer
TimeControlBookingwird erzeugt (über die Terminal-/Stempel-UI/timecontrol/mainoder über externes Polling, siehe § 14). - Nightly Job (
CreateTimeProfileWorkJournalsCommand) erzeugt für jeden aktiven Mitarbeiter ein leeresTimeProfileWorkerJournalfür den Tag, das Profil wird ausTimeProfileCalendar(oderTimeProfileGroup→ FallbackTimeProfile.is_fallback) ermittelt. CalculateTimeProfileJournalsCommandruftTimeProfileWorkerJournalCalculationProcessor::calculateJournalTimes()auf. Dieser:- lädt alle
TimeControlBookings des Tages, - validiert sie über
TimeProfileBookingErrorProcessor, - berechnet Profilzeit, bezahlte Arbeitszeit, Überstunden, Pausenkorrektur, Abwesenheiten und Saldo,
- persistiert das Ergebnis,
- triggert ggf. Benachrichtigungen.
- lädt alle
- Erkennt der Processor Fehler (z. B. fehlende Check-Out-Buchung, Buchung außerhalb aller Tagesblöcke), wird
status='invalid'gesetzt und der Doctrine-Listener TimeProfileJournalListener.php benachrichtigt den Vorgesetzten. - Im Frontend prüft/genehmigt der Vorgesetzte das Journal manuell oder löst eine Neuberechnung aus.
4. Datenmodell (Entities)
Alle Pfade relativ zu biz.Suite_Backend/. Migrationen liegen unter migrations/.
4.1 TimeProfile – src/Entity/TimeProfile.php
| Feld | Typ | Bedeutung |
|---|---|---|
id | int | PK |
name | string | Anzeigename des Modells |
description | string | Freitext |
color | string | Farbe für UI-Darstellung |
profileTime | int (Sekunden, optional) | Optionale Gesamt-Sollzeit (üblicherweise aus Day Lines berechnet) |
isFallback | bool | Falls true, wird dieses Profil verwendet, wenn ein Mitarbeiter kein spezifisch zugewiesenes Profil hat |
countBreakPartially | bool | Steuert, ob Pflichtpausen anteilig oder als Block abgezogen werden (siehe § 9.4) |
calculationPeriod | string ('0_24' | '12_12', nullable) | Berechnungsperiode des Profils. Bestimmt das Tagesfenster, über das Buchungen, Profilzeit und Tagesblöcke ausgewertet werden – siehe § 9.12. Migriert per app:migrate-time-profile-calculation-period von null auf '0_24' |
salaryAgreement | ManyToOne SalaryAgreement | Verknüpfter Lohntarif |
timeProfileDays | OneToMany TimeProfileDay | Wochentagsblöcke |
timeProfileBreakSettings | OneToMany TimeProfileBreakSetting | Schwellenwert-Regeln für Pflichtpausen |
4.2 TimeProfileDay – src/Entity/TimeProfileDay.php
| Feld | Typ | Bedeutung |
|---|---|---|
id | int | PK |
name | string | monday, tuesday, …, sunday |
timeProfile | ManyToOne | Zugehöriges Modell |
timeProfileDayLines | OneToMany | Tagesblöcke |
4.3 TimeProfileDayLine – src/Entity/TimeProfileDayLine.php
| Feld | Typ | Bedeutung |
|---|---|---|
id | int | PK |
startTime | DateTime (HH:mm) | Block-Beginn |
endTime | DateTime (HH:mm) | Block-Ende |
toleranceBeforeStart | int (Min.) | Erlaubte Stempel-Frühstempelung vor Blockbeginn |
toleranceAfterStart | int (Min.) | Erlaubte verspätete Stempelung nach Blockbeginn |
toleranceBeforeEnd | int (Min.) | Erlaubte vorzeitige Auschstempelung |
toleranceAfterEnd | int (Min.) | Erlaubte verspätete Ausstempelung |
timeProfileType | ManyToOne TimeProfileType | Steuert, wie der Block bewertet wird |
timeProfileDay | ManyToOne | Zugehöriger Tag |
4.4 TimeProfileType – src/Entity/TimeProfileType.php
Der Zeitprofiltyp ist der wichtigste Konfigurations-Hebel für die Berechnung. Jeder TimeProfileDayLine ist genau einem Typ zugeordnet, der über sieben boolesche Flags steuert, wie der Block in die Tagessummen einfließt:
| Feld | Typ | Steuert |
|---|---|---|
id | int | PK |
name | string | Anzeigename |
code | string | Klassifikator: StandardTime, Overtime, Break, PaidBreak, FlexibleWorkingHours+, FlexibleWorkingHours-, CheckIn, CheckOut |
calcStandardTime | bool | Einbeziehen in paid_working_time (Standard-Soll) |
calcRewardedTime | bool | Block ist als „entlohnte Zeit" zu zählen |
calcPaidOvertime | bool | Einbeziehen in paid_overtime |
calcJustifiedAbsence | bool | Block ist „berechtigte Abwesenheit" / geplante Pause |
calcUnauthorizedAbsence | bool | Block ist „unberechtigte Abwesenheit" |
calcFlexibleWorkingHoursPlus | bool | Positive Gleitzeitminuten |
calcFlexibleWorkingHoursMinus | bool | Negative Gleitzeitminuten |
Diese Flags werden in
TimeProfileWorkerJournalCalculationProcessordirekt abgefragt – ihr Verhalten ist in § 9 erläutert.
4.5 TimeProfileBreakSetting – src/Entity/TimeProfileBreakSetting.php
| Feld | Typ | Bedeutung |
|---|---|---|
id | int | PK |
timeLimit | int (Sek.) | Schwelle: Wenn die Arbeitszeit diesen Wert übersteigt, wird … |
breakLength | int (Sek.) | … breakLength an Pausenzeit verpflichtend |
timeProfile | ManyToOne | Zugehöriges Modell |
Mehrere Regeln werden kumuliert (siehe § 9.4).
4.6 TimeProfileGroup – src/Entity/TimeProfileGroup.php
| Feld | Typ | Bedeutung |
|---|---|---|
id | int | PK |
name, description | string | Anzeigedaten |
timeProfile | ManyToOne | Modell, das auf alle Mitglieder angewendet wird |
autoGenerateCalendarEntries | bool | Wenn true, werden über TimeProfileCalendarEntryGenerationProcessor automatisch TimeProfileCalendar-Einträge erzeugt |
allowedWeekDays | json/array | Auswahl der Wochentage, an denen die Gruppe wirksam wird |
4.7 TimeProfileGroupWorker – src/Entity/TimeProfileGroupWorker.php
Verknüpfungstabelle: timeProfileGroup ↔ worker.
4.8 TimeProfileCalendar – src/Entity/TimeProfileCalendar.php
| Feld | Typ | Bedeutung |
|---|---|---|
id | int | PK |
date | date | Konkretes Kalenderdatum |
worker | ManyToOne | Mitarbeiter |
timeProfile | ManyToOne | Für diesen Tag gültiges Modell (Override gegenüber Gruppen-Default) |
timeProfileGroup | ManyToOne (optional) | Quelle, falls automatisch generiert |
4.9 TimeProfileWorkerJournal – src/Entity/TimeProfileWorkerJournal.php
Die zentrale Entity des Features.
| Feld | Typ | Bedeutung |
|---|---|---|
id | int | PK |
worker | ManyToOne | Mitarbeiter |
timeProfile | ManyToOne | Tagsgenau gültiges Modell |
date | date | Tagesdatum |
startTime | datetime | Erster Check-In des Tages |
endTime | datetime | Letzter Check-Out des Tages |
paidWorkingTime | int (Sek.) | Tatsächlich entlohnte Arbeitszeit (max. = profileTime) |
paidOvertime | int (Sek.) | Entlohnte Überstunden (Anteil über profileTime) |
paidAbsenceTime | int (Sek.) | Bezahlte Abwesenheit (Urlaub, Krankheit, Feiertag) |
unpaidAbsenceTime | int (Sek.) | Unbezahlte Abwesenheit |
profileTime | int (Sek.) | Soll-Arbeitszeit für den Tag |
balance | int (Sek.) | Tagessaldo: paidWorkingTime + paidAbsenceTime + paidOvertime − profileTime |
balanceAccumulated | int (Sek.) | Kumulativer Saldo inkl. aller vorhergehenden, genehmigten Journale + BalanceCorrections |
unAccountedPresence | int (Sek.) | Anwesenheit außerhalb aller Tagesblöcke und ohne Genehmigung |
isCalculated | bool | Berechnung erfolgreich (kein Fehler) |
isApproved | bool | Manuell oder automatisch genehmigt |
isRebooked | bool | Buchungen wurden manuell korrigiert |
isHoliday | bool | Tag ist ein Feiertag im Bundesland des Mitarbeiters |
status | string | 'valid' / 'invalid' |
errorDescription | string | 'wrongBookings' oder 'noDayLines' |
balanceCorrections | OneToMany | Manuelle Saldokorrekturen |
timeProfileWorkerJournalEvaluations | OneToMany | Aggregation pro Buchungstyp/Standort |
4.10 TimeProfileWorkerJournalEvaluation – src/Entity/TimeProfileWorkerJournalEvaluation.php
Aggregat-Datensatz pro Journal mit Bezug zu TimeControlBookingType und Location für Reports / Lohnauswertungen.
4.11 BalanceCorrection – src/Entity/BalanceCorrection.php
Manuelle Saldokorrektur (Stunden in/aus Zeitkonto, Sonderfälle).
| Feld | Typ | Bedeutung |
|---|---|---|
balance | float | Korrektur-Sekunden (positiv/negativ) |
date | date | Wirksamkeitsdatum |
reason | string | Begründungstext |
isLocked | bool | Sperre gegen weitere Bearbeitung |
salaryType | ManyToOne | Optionaler Lohnart-Bezug |
5. TimeControlBooking – Stempelbuchungen
Stempelbuchungen sind die Roh-Eingabedaten der Journalberechnung. Eine Buchung repräsentiert ein einzelnes Ereignis (Kommen, Pause-Beginn, Pause-Ende, Gehen) mit Zeitstempel, optional GPS-Position und Standort. Aus aufeinanderfolgenden Buchungen wird der BookingEntry-DTO im Processor gebildet (siehe § 9.1).
5.1 Entity TimeControlBooking – src/Entity/TimeControlBooking.php
Tabelle: timecontrolbookings. Versionierung über Gedmo Loggable (Audit-Trail) auf gpsLat, gpsLon, bookingType, worker, entryDateTime. Verwendet die Traits SystemableEntity (mandantenfähig: sysclientid, sysforeignid) und TimestampableEntity.
| Feld | Typ | Bedeutung |
|---|---|---|
id | bigint | PK |
bookingType | ManyToOne TimeControlBookingType | Klassifiziert die Buchung (Check-In/Out/Pause). Steuert die Reihenfolgevalidierung und das Erzeugen von BookingEntrys |
worker | ManyToOne Worker | Stempler |
entryDateTime | datetime | Zeitpunkt der Buchung |
status | string | STATUS_INVALID = 'invalid' oder STATUS_APPROVED = 'approved' (gesetzt durch TimeProfileBookingErrorProcessor) |
description | string | Freitext (z. B. Kommentar im Buchungs-Dialog) |
location | ManyToOne Location (optional) | Standort – Pflicht für bookingType.isOnSite=true |
gpsLat, gpsLon | string | Geokoordinaten (vom Browser via navigator.geolocation gesetzt) |
isNew | bool | Markierung für noch nicht synchronisierte Buchungen (Mobile/Offline) |
isSynced | bool | Synchronisationsstatus |
isReRegistration | bool | true, wenn aus einem genehmigten TimeControlBookingRequest erzeugt |
Konstanten:
STATUS_INVALID = 'invalid'– mind. eine Validierung schlug fehlSTATUS_APPROVED = 'approved'– validiert und akzeptiert
5.2 Entity TimeControlBookingType – src/Entity/TimeControlBookingType.php
Tabelle: timecontrolbookingtypes.
| Feld | Typ | Wirkung |
|---|---|---|
id | bigint | PK |
name | string | Anzeige (z. B. „Kommen", „Gehen", „Pause") |
shortCode | string | Kürzel (z. B. „CI", „CO", „HO" für Home-Office) |
description | string | Freitext |
icon | string | UI-Icon-Schlüssel |
isCheckIn | bool | Validierungs-Flag: Eine Buchung mit diesem Typ startet eine Anwesenheitsperiode. Nutzbar in TimeProfileBookingErrorProcessor::validateBookingOrder() |
isCheckOut | bool | Validierungs-Flag: Beendet eine Anwesenheitsperiode. Pro System darf nur ein Typ isCheckOut=true aktiv sein (Toggle-Endpoint sorgt für Exklusivität) |
isOnSite | bool | Erzwingt im FE-Buchungs-Dialog die Eingabe einer location. Bei Stempelung im Terminal-Modus wird ggf. die GPS-Position aus der Location ergänzt |
isAvailableInTerminalMode | bool | Sichtbar im Terminal-/Kiosk-Stempel-UI (getmappedbookingtypes filtert die Check-In-Liste danach) |
Aus den booleschen Kombinationen ergeben sich konzeptionell folgende Typklassen:
- Check-In On-Site –
isCheckIn=true, isOnSite=true - Check-In Home Office –
isCheckIn=true, isOnSite=false, shortCode='HO' - Check-In On-Track (mobil/außendienst) –
isCheckIn=true, isOnSite=false, shortCode='DF' - Check-Out –
isCheckOut=true(genau ein Typ pro System) - Pause / Sonstige –
isCheckIn=false, isCheckOut=false
5.3 Entity TimeControlBookingRequest – src/Entity/TimeControlBookingRequest.php
Workflow für nachträgliche Buchungen (z. B. vergessene Stempelung). Bei Genehmigung erzeugt TimeControlBookingRequestConversionProcessor einen echten TimeControlBooking mit isReRegistration=true. Nach erfolgreicher Konvertierung ruft der zuständige Controller direkt TimeProfileWorkerJournalCalculationProcessor auf, sodass das Journal des betroffenen Tages neu berechnet wird.
| Feld | Wirkung |
|---|---|
worker, entryDateTime, bookingType, location, description | wie TimeControlBooking |
status | Workflow-Status: STATUS_CREATED, STATUS_PENDING, STATUS_PRETESTED, STATUS_APPROVED, STATUS_DENIED |
reason | Begründung bei Ablehnung |
Statusübergänge werden durch TimeControlBookingRequestListener (prePersist/preUpdate) mit Notifications an den Antragsteller, Vorgesetzten und ggf. die Personalstelle versehen.
5.4 Repository TimeControlBookingRepository – src/Repository/TimeControlBookingRepository.php
Spezielle Query-Methoden, die im Berechnungsprozess relevant sind:
| Methode | Zweck |
|---|---|
findByWorkerForToday(Worker, DateTime $start, DateTime $end, string $type='all') | Wird vom TimeProfileWorkerJournalCalculationProcessor aufgerufen. Lädt alle Buchungen eines Mitarbeiters für ein Tagesfenster, optional nach Status gefiltert. |
findByWorkerForDate(Worker, Carbon $date) | Gleiche Idee, abstrahiert über Carbon |
findByWorkerByDate(Worker, Request) | Via HTTP-Request-Parameter |
findInRangeFiltered(Carbon $start, Carbon $end, ?Worker, ?string $type, bool $sortAscending=true) | Konsolidierte Allzweck-Query |
findLastCheckInBooking(Worker) | Letzte Check-In-Buchung – Basis für die laufende Stempel-Anzeige |
getLastBookingPerWorker(Worker) | Letzte Buchung mit Flags is_check_in/is_check_out |
findAllBookingsForToday() | Heute, mandantenweit (Dashboard-Aggregation) |
5.5 Controller
TimeControlBookingController – src/Controller/TimeControlBookingController.php
| Methode | Pfad | Zweck |
|---|---|---|
| GET | /api/timecontrolbookings | Listing (paginiert) – Query: worker, date, type |
| POST | /api/timecontrolbookings | Anlegen (Hauptpfad). Triggert handleWorkBookingStatus() und ggf. checkTimeControlWorkStatus(), um offene Aufträge automatisch zu schließen |
| POST | /api/timeControlBookingForce | Anlegen ohne Duplikat-/Matching-Prüfung (Admin-Override) |
| PUT | /api/timecontrolbookings/{id} | Aktualisieren |
| DELETE | /api/timecontrolbookings/{id} | Löschen |
| GET | /api/getcurrentbookingstatus | Letzte Buchung des aktuellen Users (oder simulierten Check-Out) |
| GET | /api/getRawBookingTime | Verstrichene Zeit seit letztem Check-In (Industrie-/Stunden-Format) |
| GET | /api/bookingstatusbyworker | Letzte Buchung pro aktivem Mitarbeiter (Dashboard) |
| GET | /api/getTimeControlStatus | Aggregat: checkInOnSite, checkInHomeOffice, checkInOnTrack, pause, checkedOut + aktive Abwesenheiten |
| GET | /api/dashboard/numbers | Kennzahlen: present/absent/on_break/on_vacation |
| GET | /api/bookingstatusbyterminalid | Lookup über Terminal-Seriennummer |
Im createOrUpdate(Request) (privat) gilt:
- Bei neuer Buchung wird der
workeraus dem aktuellen User abgeleitet, wenn nicht explizit gesetzt. entryDateTimedefaultet auf „jetzt", wenn fehlend.- Duplikat-Schutz: Identische Buchung (gleicher Worker + gleicher Typ kurz vorher) wird nicht doppelt angelegt – die existierende wird zurückgegeben.
- Wenn
locationgesetzt ist und (Terminal-Modus oder fehlende GPS-Koordinaten), werden GPS-Werte aus der Location ergänzt.
TimeControlBookingTypeController – src/Controller/TimeControlBookingTypeController.php
| Methode | Pfad | Zweck |
|---|---|---|
| GET/POST/PUT/DELETE | /api/timecontrolbookingtypes[/{id}] | CRUD |
| GET/PUT | /api/switchcheckinbookingtype/{id} | Toggle isCheckIn |
| GET/PUT | /api/switchcheckoutbookingtype/{id} | Toggle isCheckOut (deaktiviert dabei alle anderen Check-Out-Typen) |
| GET | /api/getmappedbookingtypes | Kategorisierte Liste: isCheckIn[] (gefiltert nach Terminal-Modus), isCheckOut, others, locations, default_location |
| GET | /api/timeControlBookingTypes/byType?checkIn=…&checkOut=… | Filterquery |
TimeControlBookingRequestController – src/Controller/TimeControlBookingRequestController.php
CRUD für TimeControlBookingRequest. Der PUT-Endpoint behandelt Genehmigung/Ablehnung und ruft bei STATUS_APPROVED den TimeControlBookingRequestConversionProcessor sowie anschließend den TimeProfileWorkerJournalCalculationProcessor auf.
TimeControlBookingReportController
GET /api/reports/timecontrolbooking – PDF-Report über TimeControlBookingReportProcessor.
5.6 Processoren / Listener mit TimeControlBooking-Bezug
| Klasse | Zweck |
|---|---|
| TimeProfileBookingErrorProcessor | Validierung der Buchungs-Reihenfolge und der Tagesblock-Zugehörigkeit; setzt status pro Buchung |
| TimeControlBookingRequestConversionProcessor | Genehmigte Request → echter TimeControlBooking |
| TimeBookingDashboard | Aggregation pro Worker für Dashboards (currentBookingByWorker, currentBookingByWorkerWithoutImage) |
| TimeControlBookingReportProcessor | PDF-Reporting |
| TimeControlBookingRequestListener | Doctrine prePersist/preUpdate → Notifications |
5.7 ViewModels / DTOs
- TimeControlBookingDashboardData – Container für
checkInOnSite,checkInHomeOffice,checkInOnTrack,pause,checkedOut,absence - TimeControlBookingDashboardLine – Liste von Bookings +
name+amountOfWorkers - TimeControlBookingReport – PDF-Report-DTO
5.8 Frontend-Modell TimeControlBooking – TimeControlBooking.model.ts
| Property | Typ |
|---|---|
id | string |
booking_type | TimeControlBookingType |
entry_date_time | string (ISO 8601) |
is_new, is_synced | boolean |
gps_lat, gps_lon | string |
worker | Worker |
status | string |
description | string |
location | Location | null |
duration? | number (FE-only, virtuell, in Millisekunden) |
TimeControlBookingType – bookingtype.model.ts:
| Property | Typ |
|---|---|
id, short_code, name, description, icon | string |
isCheckIn, isCheckOut, isOnSite, isAvailableInTerminalMode | boolean |
5.9 Frontend-Services
TimeControlService – timecontrol.service.ts
| Methode | Endpunkt | Zweck |
|---|---|---|
getBookingTypes() | GET /api/getmappedbookingtypes | Kategorisiert nach checkInTypes (Terminal-gefiltert), checkOutType, otherTypes |
getBookingStatus() | GET /api/getcurrentbookingstatus | Aktueller Status; bei Fehlen → simulierter Check-Out |
getRawBookingTime() | GET /api/getRawBookingTime | Verstrichene Zeit, RxJS-Stream |
getLocationService() | – | Browser-Geolocation-API |
createNewBooking(body) | POST /api/timecontrolbookings | Buchung anlegen |
Subjects: onBookingTypesChanged, onBookingStatusChanged.
TimeControlBookingTypeService – bookingtypes.service.ts
| Methode | Endpunkt |
|---|---|
findBookingTypes() | GET /api/timecontrolbookingtypes |
deleteBookingType(id) | DELETE /api/timecontrolbookingtypes/{id} |
switchIsCheckIn(id) | GET /api/switchcheckinbookingtype/{id} |
switchIsCheckOut(id) | GET /api/switchcheckoutbookingtype/{id} |
switchIsOnSite(bookingType) | PUT /api/timecontrolbookingtypes/{id} |
switchIsAvailableInTerminalMode(bookingType) | PUT /api/timecontrolbookingtypes/{id} |
TimeProfileWorkerDetailsV2Service – Buchungen im Journal-Detail
Methoden getTimeControlBookingsForJournal(journalId), postBooking(data), putBooking(data), deleteBooking(id) rufen:
GET /api/v2/timeProfileWorkerJournals/events/{id}– Buchungen + Abwesenheiten eines TagesPOST /api/timecontrolbookings?admin=true– Manuelle Anlage durch HR/AdminPUT /api/timecontrolbookings/{id}?admin=true– Manuelle KorrekturDELETE /api/timecontrolbookings/{id}– Löschung
5.10 Frontend-Komponenten zu Stempelbuchungen
| Komponente | Datei | Zweck |
|---|---|---|
TimeControlComponent | timecontrol.component.ts | Stempel-UI / Terminal unter /timecontrol/main. Properties: currentBookingType, currentBooking, checkInTypes, checkOutType, otherTypes, locations, rawBookingTime, isLocked. Methoden: getBookingTypes(), getCurrentBookingType(), createNewBooking(type, location?), checkLocation(type), startTimer(), pauseTimer(), transform(value, mode) (Format Industrie-/Stunden) |
TimeProfileWorkerBookingDialogV2Component | time-profile-worker-v2/time-profile-worker-details-v2/time-profile-worker-booking-dialog-v2/ | Dialog im Journal-Detail zum Anlegen/Bearbeiten einer Buchung. Form-Controls: bookingType (required), location (required wenn isOnSite), startTime (required), comment (required) |
TimeProfileWorkerBookingItemV2Component | time-profile-worker-v2/time-profile-worker-details-v2/time-profile-worker-booking-item-v2/ | Eine Buchungszeile im Journal mit Edit/Delete (sichtbar via actionsAllowed()) |
BookingtypesComponent | time-control-settings/bookingtypes/ | Settings-Liste; Spalten: shortcode, name, description, icon, checkin, checkout, isOnSite, isAvailableInTerminalMode, edit |
TimecontrolbookingtypeDialogComponent | time-control-settings/bookingtypes/bookingtype-dialogs/ | Create/Edit Buchungstyp |
5.11 End-to-End-Flow am Stempel-Terminal
TimeControlComponent.ngOnInit()ruftgetBookingTypes()→GET /api/getmappedbookingtypes→ Kategorisiert incheckInTypes(gefiltert nachisAvailableInTerminalMode),checkOutType,otherTypes.getCurrentBookingType()→GET /api/getcurrentbookingstatus→ letzte Buchung oder simulierter Check-Out → Timer wird gestartet.- Nutzer klickt einen Buchungs-Typ. Falls
isOnSite=trueundlocationnoch nicht gewählt: Standortauswahl erzwingen. - Browser-Geolocation abfragen (
navigator.geolocation.getCurrentPosition),gps_lat/gps_lonsetzen. POST /api/timecontrolbookingsmit Payload:{
"booking_type": { "id": "<typeId>" },
"is_new": true,
"is_synced": false,
"gps_lat": "<lat>",
"gps_lon": "<lon>",
"location": { "id": "<locationId>" }
}- Backend:
TimeControlBookingController::createOrUpdate()→ Duplikat-Prüfung, Worker-/Datums-Defaults, ggf. GPS aus Location, Persist. handleWorkBookingStatus()schließt offene Auftragszeiten/Pausen, falls Anwesenheitsstatus wechselt.- FE ruft erneut
getCurrentBookingType(), aktualisiert Anzeige, entsperrt das UI (isLocked=false).
6. Backend-Komponenten
6.1 Processoren – src/Processor/TimeProfiles/
| Klasse | Zweck |
|---|---|
| TimeProfileWorkerJournalCalculationProcessor.php | Hauptberechnung pro Journal |
| ProfileTimeCalculationProcessor.php | Errechnet profileTime (Soll) eines TimeProfileDay als Summe aller Day-Lines mit calcStandardTime=true |
| TimeProfileWorkerJournalBalanceCalculationProcessor.php | Akkumulierten Saldo neu rechnen (aktuelles Journal + alle nachfolgenden genehmigten Journale + BalanceCorrections) |
| TimeProfileBookingErrorProcessor.php | Markiert Buchungen als gültig/ungültig (validateBookingOrder, validateBookingPeriod) |
| TimeProfileWorkNotificationProcessor.php | Benachrichtigung bei unerfasster Präsenz |
| TimeWorkerJournalsCreationProcessor.php | Batch-Anlage von leeren Journalen |
| TimeWorkerJournalsDeletionProcessor.php | Batch-Bereinigung |
| BookingEntry.php | DTO eines Buchungs-Intervalls (Start, Ende, isPresent, isBreakTime, verbleibende Zeit) |
| TimeProfileCalendarEntryGenerationProcessor.php | Auto-Generierung von TimeProfileCalendar-Einträgen |
| TimeProfileWorkerJournalEvaluationProcessor.php | Aggregation pro Standort/Buchungstyp |
| TimeProfileWorkerJournalReportProcessor.php | Wochen-/Monatsaggregation für Reports |
6.2 Wochenreport-Aggregation – src/TimeProfileWorkerJournalWeekReport/
| Klasse | Zweck |
|---|---|
TimeProfileWorkerJournalWeekReportProcessor | Erzeugt aggregiertes Wochen-Reporting |
WeekData | DTO Woche |
WorkerData | DTO Mitarbeiter |
6.3 Repositorys – src/Repository/
TimeProfileRepository, TimeProfileDayRepository, TimeProfileDayLineRepository, TimeProfileTypeRepository, TimeProfileBreakSettingRepository, TimeProfileGroupRepository, TimeProfileGroupWorkerRepository, TimeProfileCalendarRepository, TimeProfileWorkerJournalRepository, TimeProfileWorkerJournalEvaluationRepository, TimeControlBookingRepository, TimeControlBookingTypeRepository, TimeControlBookingRequestRepository, BalanceCorrectionRepository.
Wichtige Custom-Methoden im TimeProfileWorkerJournalRepository:
findPreviousApprovedJournal(date, worker)– letztes genehmigtes Journal vor Datum (Saldo-Basis)findNextApprovedJournals(journal)– Folgejournale für Saldo-Recalc- Pagination-/Filter-Querys nach Status, Department, Team, Location, Geschäftsbereich
6.4 Event Listener
TimeProfileJournalListener.php – Doctrine preUpdate auf TimeProfileWorkerJournal. Wird der Status auf 'invalid' gewechselt, wird über NotificationProcessor der Vorgesetzte informiert.
TimeControlBookingRequestListener.php – prePersist und preUpdate für nachträgliche Buchungs-Anträge mit Notifications an Antragsteller / Vorgesetzten / Personal.
6.5 Suchindizes (Elastica)
In config/packages/fos_elastica.yaml sind indiziert:
timeprofiletimeprofiletypetimeprofilegroup(Propertyallowed_week_days)
7. Frontend-Komponenten
Alle Pfade relativ zu biz.Suite_Frontend/src/app/main/apps/timecontrol/.
7.1 Routing (timecontrol.module.ts)
/timecontrol/
├── time-profile-worker-journals → TimeProfileWorkerV2Module
│ guard: requirePermission('time_profile_journal_access')
├── settings → TimeControlSettingsModule
│ guards je Sub-Route: requirePermission('timecontrol_settings'|'timecontrol_journals')
├── main → TimeControlMainModule (Stempel-UI)
│ guard: requirePermission('timecontrol_user')
├── work → TimeControlWorkModule (Auftragszeit)
│ guards: requirePermission('projects_READ'), requirePermission('timecontrol_user')
└── invoice → InvoiceModule
guard: requirePermission('timecontrol_user')
7.2 V2-Modul Arbeitszeitsjournale – time-profile-worker-v2/
| Komponente | Zweck |
|---|---|
TimeProfileWorkerV2Component | Container mit Sidebar |
TimeProfileWorkerListV2Component | Listenansicht aller Journale (paginiert, gefiltert) – Resolver resolveJournals |
TimeProfileWorkerDetailsV2Component | Detailansicht eines Journals: Arbeitszeit, Pause, Saldo, Differenz, Profilzeit; Aktionen Genehmigen, Berechnen, Unerfasste Präsenz akzeptieren/ablehnen, Buchung hinzufügen |
TimeProfileWorkerFilterSidebarV2Component | Filter (Datum, Geschäftsbereich, Abteilung, Team, Mitarbeiter, Status) |
TimeProfileWorkerBookingItemV2Component | Einzelne TimeControlBooking-Zeile |
TimeProfileWorkerAbsenceItemV2Component | Abwesenheitszeile |
TimeProfileWorkerHolidayItemV2Component | Feiertags-Indikator |
TimeProfileWorkerBookingOverviewV2Component | „Übersicht"-Tab (Aggregat) |
TimeProfileWorkerBookingDialogV2Component | Dialog zum Anlegen/Bearbeiten einer Buchung |
Property-Map TimeProfileWorkerDetailsV2Component:
selectedJournal,totalWorkTime,totalBreakTime,totalDiffTime,profileTime,balanceviewType: 'bookings' | 'overview'(Toggle „Zeiten" / „Übersicht")actionsAllowed()prüft Permissions (siehe § 12)- Aktionen:
toggleApprovedFlag(),calculateJournal(),declineUnAccountedPresence(),approveUnAccountedPresence(),openDialog()
7.3 Konfigurationsmodule – time-control-settings/ und time-profiles/
| Route | Komponente | Zweck |
|---|---|---|
time-profiles | TimeProfileComponent + Detail/Tabs Mo–So + TimeProfileDayLineDialogComponent | Arbeitszeitmodelle pflegen |
time-profile-types | TimeProfileTypeComponent + 3-Schritt-Stepper-Dialog | Berechnungs-Klassen pflegen |
time-profile-groups | TimeProfileGroupComponent + Kalender-Subview + Worker-Zuordnungen | Schichtplangruppen |
booking-types | BookingtypesComponent | Stempel-Buchungstypen |
time-chips | TimechipsComponent | Physische Karten |
worker-chips | WorkerchipsComponent | Karten ↔ Mitarbeiter |
basic-settings | TimeControlBasicSettingsComponent | Globale Schalter (Tabelle Name/Modus) |
journals | TimeProfileJournalModule (Settings-Variante) | Journal-Konfiguration |
7.4 Services
| Service | Verantwortung |
|---|---|
TimeProfileJournalService (time-profile-worker/time-profile-journal.service.ts) | V1-Listing/Filter, calculate(), update(), getClosedMonths() |
TimeProfileWorkerV2Service, TimeProfileWorkerDetailsV2Service, TimeProfileWorkerFilterV2Service | V2-Listing, Detail, Filter; ruft u. a. GET /api/v2/timeProfileWorkerJournals/events/{id} für Buchungen |
TimeProfileService | CRUD TimeProfile, BreakSettings |
TimeProfileGroupService | CRUD TimeProfileGroup, Worker-Zuordnung, startCalendarEntryGeneration() |
TimeProfileCalendarService | CRUD TimeProfileCalendar |
TimeProfileTypeService | CRUD TimeProfileType |
BookingtypesService, TimechipsService, WorkerchipsService | CRUD Settings |
TimecontrolService | Stempel-UI: Buchung anlegen, Geolocation |
TimeControlBasicSettingsService | is_active-Toggle pro Setting |
7.5 Models / Interfaces
Frontend-Typen liegen jeweils neben den zugehörigen Komponenten:
- TimeProfile.model.ts,
TimeProfileDay.model.ts,TimeProfileDayLine.model.ts - TimeProfileWorkerJournal.model.ts und time-profile-worker-types.ts
TimeProfileType.model.ts,TimeProfileGroup.model.ts,TimeProfileCalendar.model.ts- TimeControlBooking.model.ts, bookingtype.model.ts,
timechip.model.ts,workerchip.model.ts
Filter-Typen in time-profile-worker-types.ts:
type TimeProfileJournalOwnerFilter = 'all' | 'my';
type TimeProfileWorkerModelFilter = 'all' | 'unapproved_or_invalid' | 'only_unapproved' | 'only_invalid';
8. Konfiguration & alle FE-Einstellungen / Schalter
Dieser Abschnitt listet alle UI-Einstellungen der Module auf und beschreibt ihre Auswirkung auf die Berechnung.
8.1 TimeProfile-Dialog – Arbeitszeitmodell
Datei: time-profiles/time-profile-dialogs/time-profile-dialog/
| Form-Control | Typ | Label DE | Wirkung in der Berechnung |
|---|---|---|---|
name | text | Name | Anzeige |
description | text | Beschreibung | Anzeige |
color | Farbpicker | Farbe | UI-Darstellung in Liste/Kalender |
profile_time | number | Profilzeit | Optional manuell. Wird normalerweise aus Day Lines berechnet (siehe ProfileTimeCalculationProcessor::calculateProfileTime). |
is_fallback | Toggle | Ist Fallback | Steuert die Profilauswahl bei Tagesanlage: Mitarbeiter ohne expliziten TimeProfileCalendar-Eintrag erhalten dieses Modell. |
count_break_partially | Toggle | Pause teilweise zählen | Verändert die Pflichtpausenkorrektur: Bei true wird die Pflichtpause anteilig zur tatsächlich überschrittenen Schwelle abgezogen (min(diff, breakLength)); bei false wird die volle breakLength jeder erfüllten BreakSetting aufaddiert. Siehe § 9.4. |
calculation_period | Select (required) | Berechnung | Bestimmt das Tagesfenster der gesamten Journal-Berechnung. Werte: '0_24' (0–24 Modell, kalendarischer Tag) oder '12_12' (12–12 Modell, Mittag-zu-Mittag). Wirkt durchgängig auf Buchungs-Lookup (findInRangeFiltered), Day-Line-Projektion (getAdjustedDate), Profilzeit-Berechnung sowie auf das FE (Sortierung von Day-Lines, Journal-Detail-Header mit zwei Daten, Buchungs-Dialog mit Vortags-Rückdatierung). Siehe § 9.12. |
salary_agreement | Suche | Lohnvereinbarung | Verknüpfung für Lohnabrechnungen |
8.2 TimeProfileBreakSetting-Dialog – Pflichtpausen
| Form-Control | Typ | Label DE | Wirkung |
|---|---|---|---|
time_limit | Sekunden | Schwelle | Wenn (geleistete Arbeitszeit) ≥ Schwelle, wird break_length verpflichtend |
break_length | Sekunden | Pausenlänge | Soll-Pausenlänge bei dieser Schwelle |
Mehrere Regeln pro Profil werden kumulativ angewandt (z. B. „Nach 6h: 30min", „Nach 9h: 45min"). Der Algorithmus in calculateBreakBasedOnBreakSettings() sortiert nach timeLimit aufsteigend und addiert die Sollpausen so lange auf, wie die Schwelle erreicht ist – ggf. anteilig (siehe count_break_partially).
8.3 TimeProfileDayLine-Dialog – Tagesblock
Datei: time-profile-day-line-dialog
| Form-Control | Typ | Label DE | Wirkung |
|---|---|---|---|
start_time | Zeit (HH:mm) | Startzeit | Block-Beginn; relevant für Überlappungsprüfung mit Buchungen |
end_time | Zeit (HH:mm) | Endzeit | Block-Ende |
tolerance_before_start | int (Min.) | Toleranz vor Start | Stempelzeit vor dem Blockbeginn wird ignoriert (subtrahiert), wenn sie innerhalb dieser Toleranz liegt – siehe isWithinTolerance() |
tolerance_after_start | int (Min.) | Toleranz nach Start | (Schalter im Modell vorhanden; aktuell mit // FIXME: Check for tolerance after start and before end annotiert) |
tolerance_before_end | int (Min.) | Toleranz vor Ende | (siehe tolerance_after_start) |
tolerance_after_end | int (Min.) | Toleranz nach Ende | Stempelzeit nach dem Blockende wird ignoriert (subtrahiert), wenn sie innerhalb dieser Toleranz liegt |
time_profile_type | Select | Profiltyp | Bestimmt die Bewertung des Blocks (siehe § 8.4) |
Bei
time_profile.calculation_period === '12_12'werden Day-Lines mitstart_time ≥ 12:00im FE als datierte Werte (Date-String inkl. „heute"-Datum) gehalten und übernew Date(...).getTime()sortiert; Day-Lines mitstart_time < 12:00bleiben als reinerHH:mm-String erhalten. Im Backend werden diese Zeiten ingetAdjustedDate()zusätzlich auf den Vortag projiziert, damit die Schicht über die Mittagsgrenze hinweg konsistent bewertet wird (siehe § 9.12.2).
8.4 TimeProfileType-Dialog – Zeitprofiltyp (3-Schritt-Stepper)
Datei: time-control-settings/time-profile-type/time-profile-type-dialog/
Schritt 1: Basisdaten
| Form-Control | Typ | Label DE | Wirkung |
|---|---|---|---|
name | Text (max 60) | Name | Anzeige |
code | Select | Code | Klassifikator – benutzt u. a. von Berichten/Lohnauswertung. Werte: StandardTime, Overtime, Break, PaidBreak, FlexibleWorkingHours+, FlexibleWorkingHours-, CheckIn, CheckOut |
Schritt 2: Berechnungs-Schalter (calculationForm)
Diese sieben Toggles sind die Hauptsteuerung der Tagesberechnung. Pro Tagesblock entscheidet das Flag-Set seines Typs, welcher Topf der Tagessummen den Block aufnimmt.
| Form-Control | Label DE | Wirkung in TimeProfileWorkerJournalCalculationProcessor::calculatePaidTime() |
|---|---|---|
calc_standard_time | Standardzeit | Block fließt in paid_working_time ein. Ohne dieses Flag (oder calc_rewarded_time) wird der Block in der bezahlten Berechnung vollständig übersprungen. Außerdem: nur Blöcke mit diesem Flag tragen zur Profilzeit (Soll) bei (siehe ProfileTimeCalculationProcessor::calculateProfileTime). |
calc_rewarded_time | Entlohnte Zeit | Block wird ebenfalls in der Berechnung berücksichtigt (zusätzlicher Pass-Filter neben calc_standard_time), zählt aber nicht auf die Profilzeit. |
calc_paid_overtime | Entlohnte Überstundenzeit | Block fließt in paid_overtime ein (Über-Soll). |
calc_justified_absence | Berechtigte Abwesenheit | Block ist eine geplante Pause / berechtigte Abwesenheit. Wird verwendet, um Pausenzeiten zu summieren (totalBreakPlanned) und um unerfasste Pausen-Präsenz zu konsumieren (consumeUnplannedBreakTimePresence). Ohne dieses Flag wird der Block als nicht geplante Pause (totalBreakNotPlanned) gewertet. |
calc_unauthorized_absence | Unberechtigte Abwesenheit | Block fließt in unpaid_absence_time. |
calc_flexible_working_hours_plus | Gleitzeit+ | Block trägt zu paid_overtime bei (zusammen mit calc_paid_overtime). |
calc_flexible_working_hours_minus | Gleitzeit− | Block wird als negative Gleitzeit erfasst (Saldo-Reduktion). |
Wichtig: Mehrere Flags können auf demselben Typ aktiv sein – z. B.
calc_rewarded_timeundcalc_paid_overtime. Die Berechnung wertet sie additiv im Rahmen ihrer jeweiligen Töpfe aus.
Schritt 3: Entlohnungs-Schalter (rewardedForm)
Diese sieben Schalter steuern die Lohnauswertung (Schnittstelle zum Lohn-/Salary-Modul). Sie haben keinen direkten Einfluss auf paid_working_time/paid_overtime, sondern werden in den Reports/Salaries ausgelesen.
| Form-Control | Label DE | Wirkung |
|---|---|---|
rewardedTime | Entlohnte Zeit | Lohnrelevante Zeit |
paidOvertime | Entlohnte Überstundenzeit | Lohnrelevante Überstunden |
break | Pause | Pause wird vergütet |
flexibleWorkingHours | Gleitzeit− | Negative Gleitzeit lohnrelevant |
flexibleWorkingHoursPlus | Gleitzeit+ | Positive Gleitzeit lohnrelevant |
allowances | Zulagen | Zuschläge berücksichtigen |
absence | Abwesenheit | Abwesenheiten lohnrelevant |
8.5 TimeProfileGroup-Dialog – Schichtplan
| Form-Control | Typ | Label DE | Wirkung |
|---|---|---|---|
name, description | text | Name / Beschreibung | Anzeige |
time_profile | Select | Zeitprofil | Modell, das Mitgliedern zugewiesen wird |
auto_generate_calendar_entries | Toggle | Kalendereinträge automatisch generieren | Wenn aktiv, erzeugt TimeProfileCalendarEntryGenerationProcessor automatisch tagesgenaue TimeProfileCalendar-Datensätze (gem. allowed_week_days) |
allowed_week_days | Multi-Select | Erlaubte Wochentage | Schränkt die Auto-Generierung auf bestimmte Wochentage ein |
8.6 TimeProfileCalendar-Dialog – Kalendereintrag
| Form-Control | Typ | Label DE | Wirkung |
|---|---|---|---|
date | date | Datum | Konkreter Tag |
worker | Select | Mitarbeiter | Empfänger |
time_profile | Select | Zeitprofil | Tagsgenauer Override (höchste Priorität bei der Profilauswahl) |
time_profile_group | Select (optional) | Gruppe | Zur Nachverfolgung der Quelle |
Hilfskomponente: CopyTimeProfileCalendarDaysComponent zum Kopieren von Kalendereinträgen über Zeiträume.
8.7 Buchungs-Dialog (V2) – TimeProfileWorkerBookingDialogV2
| Form-Control | Typ | Label DE | Wirkung |
|---|---|---|---|
bookingType | Autocomplete (required) | Buchungstyp | Wählt TimeControlBookingType (Check-In/Check-Out/Pause/…) – steuert Validierung in TimeProfileBookingErrorProcessor |
location | Select (conditional) | Ort | Pflicht, wenn bookingType.is_on_site === true |
startTime | Zeit (HH:mm, required) | Zeit | Buchungszeitstempel |
comment | Text (required) | Kommentar | Begründungstext |
Wird über TimeProfileWorkerDetailsV2Service.postBooking(...) als POST /api/timecontrolbookings?admin=true an das Backend gesendet (admin=true markiert manuelle Anlage durch HR/Admin – serverseitige Permission siehe § 12).
8.8 TimeControlBookingType-Dialog – Buchungstyp
| Form-Control | Typ | Label DE | Wirkung |
|---|---|---|---|
name | text | Name | Anzeige |
shortcode | text | Kürzel | Berichts-Spalte |
description | text | Beschreibung | Anzeige |
icon | Select | Icon | UI-Symbol |
isOnSite | Toggle | Vor-Ort | Erzwingt location-Pflicht im Buchungs-Dialog |
inTerminalMode | Toggle | Anzeige Terminalmodus | Schaltet Buchungstyp im Terminal-Stempel-Modus sichtbar |
is_check_in / is_check_out | Toggle (Switch-Endpoint) | Check-In/Out | Steuert Reihenfolgevalidierung in TimeProfileBookingErrorProcessor::validateBookingOrder(). Tag muss mit Check-In starten und mit Check-Out enden, dazwischen alternierend. Genau ein Typ darf systemweit is_check_out=true sein |
8.9 Time-Chips & Worker-Chips
TimechipDialog: number, externalnumber, serial_number.
WorkerchipsDialog: worker, timechip, handoverdate, submissiondate.
Diese Einstellungen beeinflussen nur die Identifikation am Stempelterminal – keine Auswirkung auf die Tagesberechnung.
8.10 TimeControlBasicSettings
time-control-basic-settings.component.ts zeigt eine Tabelle aller globalen Settings; per updateSetting(setting) wird das Flag is_active umgeschaltet. Spalten: name, mode. Suche via searchInput.
8.11 Listen-Filter-Sidebar (TimeProfileWorkerFilterSidebarV2Component)
Filter, die an die API durchgereicht werden:
query(Volltext)mode:all/unapproved_or_invalid/only_unapproved/only_invalidstartDate,endDate,month,yearbusinessAreas[],departments[],locations[],teams[]onlyMy(Toggle „Eigene Journale")
8.12 Detailansicht – Aktionen
Im TimeProfileWorkerDetailsV2Component (View-Toggle bookings/overview):
| Button | Sichtbar wenn | Aktion |
|---|---|---|
Genehmigen-Toggle (check_circle) | actionsAllowed() | toggleApprovedFlag() ↔ setzt is_approved |
Berechnen (calculate) | actionsAllowed() | Ruft GET /api/timeprofileworkerjournals/{id}/calculate |
Unerfasste Präsenz ablehnen (cancel) | actionsAllowed() && totalDiffTime > 0 | declineUnAccountedPresence() |
Unerfasste Präsenz genehmigen (check_circle) | actionsAllowed() && totalDiffTime > 0 | approveUnAccountedPresence() – führt zu paidOvertime += unAccountedPresence (siehe § 9.6) |
Buchung hinzufügen (add) | actionsAllowed() | Öffnet TimeProfileWorkerBookingDialogV2 |
actionsAllowed() ist die zentrale Sichtbarkeits-Funktion für alle Aktionen und wird in § 12 detailliert beschrieben.
9. Berechnungslogik im Detail
Die gesamte Berechnung eines Journals geschieht in TimeProfileWorkerJournalCalculationProcessor::calculateJournalTimes(). Sie läuft in folgenden Phasen ab:
9.1 Vorbereitung
- Lese aus dem Journal:
date,worker,timeProfile. - Wochentag (
monday…sunday) bestimmen →TimeProfileDayund alleTimeProfileDayLines laden, sortiert nachstartTime. - Tagesfenster ermitteln und alle
TimeControlBookings überTimeControlBookingRepository::findInRangeFiltered($start, $end, $worker, null, true)lesen. Das Fenster hängt vontimeProfile.calculationPeriodab:'12_12'→[date−1d 12:00 ; date 12:00)(Mittag-zu-Mittag)- sonst (
'0_24'/null) →[date 00:00 ; date 23:59:59](Standard-Tag) Details siehe § 9.12.
- Validierung über
TimeProfileBookingErrorProcessor::findAndFlagErrors():- Reihenfolge (
validateBookingOrder): Erste Buchung muss Check-In sein, danach alternierend Check-Out/Pause ↔ Check-In, letzte Buchung muss Check-Out sein. Verstöße markieren die Buchung mitSTATUS_INVALID. - Periode (
validateBookingPeriod): Jede Buchung muss in eineTimeProfileDayLinefallen (sonstinvalid, sofern nichtunAccountedPresenceClearedvom FE gesetzt ist). - Hat eine Buchung Fehler, wird
journal.status = 'invalid'underrorDescription = 'wrongBookings'(oder'noDayLines', wenn das Profil für den Wochentag keine Day-Lines hat).
- Reihenfolge (
BookingEntry-DTOs aus aufeinanderfolgenden Buchungspaaren erstellen, jeweils mit:startDate,endDateisPresent:true, wenn das erste Pair-Element ein Check-In istisBreakTime:true, wenn das erste Pair-Element weder Check-In noch Check-Out ist (d. h. eine Pausen-Buchung)
9.2 Profilzeit (Soll)
profileTime = Σ (endTime − startTime) aller TimeProfileDayLines mit timeProfileType.calcStandardTime = true. Implementiert in ProfileTimeCalculationProcessor::calculateProfileTime().
Korrektur für calculationPeriod = '12_12': Liegt eine Day-Line so, dass sie vor 12:00 endet, ihr Start jedoch ≥ 12:00 ist (d. h. der Block überspannt Mitternacht innerhalb des 12-zu-12-Fensters), wird der startTime für die Differenzbildung um einen Tag zurückdatiert (startTime = startTime − 1 Tag). Dadurch wird die Dauer korrekt als positiver Zeitraum über die Tagesgrenze hinweg berechnet. Bei '0_24'/null greift diese Sonderbehandlung nicht.
9.3 Bezahlte Arbeitszeit & Überstunden
Iteration über jede TimeProfileDayLine (sortiert nach Startzeit):
für jede DayLine i:
Type = dayLine.timeProfileType
wenn !Type.calcStandardTime && !Type.calcRewardedTime:
überspringen
überlappende BookingEntries finden (Start/Ende schneidet Block)
für jeden BookingEntry in Block:
wenn !isPresent: überspringen
wenn isWithinTolerance(dayLines, i, entry): überspringen ← Toleranzlogik
Schnitt = min(entry.end, dayLineEnd) − max(entry.start, dayLineStart)
entry.subTime(Schnitt) ← Restzeit verringern
Subtotal += Schnitt
wenn Type.calcStandardTime: paidWorkingTime += Subtotal
sonst wenn Type.calcFlexibleWorkingHoursPlus
|| Type.calcPaidOvertime: paidOvertime += Subtotal
Toleranzlogik (isWithinTolerance):
- Für eine Buchung vor dem nächsten Block (
nextDayLine = dayLines[i+1]): Liegt der Buchungsstart innerhalb vonnextDayLine.toleranceBeforeStartMinuten vor dessen Start, wird die gesamte Buchungs-Restzeit konsumiert und die Schleife für diesen Eintrag beendet (zählt nicht als bezahlte Zeit). - Für eine Buchung nach dem vorigen Block (
previousDayLine = dayLines[i-1]): Analog mitpreviousDayLine.toleranceAfterEnd. - Diese Mechanik kürzt überflüssige Stempelzeit am Schichtbeginn/-ende.
9.4 Pausenberechnung
Zwei Pfade je nach Konfiguration des Modells:
A) Profil hat keine TimeProfileBreakSettings (statisches Modell)
calculateTotalBreakTime() summiert:
totalBreakPlanned: Pausen-BookingEntrys, die in einem Block mitcalcJustifiedAbsence = trueliegentotalBreakNotPlanned: Pausen-BookingEntrys in Blöcken ohne dieses FlagtotalPlannedWithinPresence: Anwesenheit oder Pausen innerhalb eines geplanten Pausenblocks
Korrektur: correction = max(totalPlannedWithinPresence − totalBreakNotPlanned − totalBreakPlanned, 0). Diese Korrektur wird von paidWorkingTime abgezogen – sie repräsentiert „Anwesenheit während geplanter Pause, die nicht durch eine echte Pausenstempelung gedeckt ist".
B) Profil hat TimeProfileBreakSettings (dynamisches Modell)
TimeProfileWorkerJournalCalculationProcessor::calculateBreakBasedOnBreakSettings():
breakSettings sortieren nach timeLimit aufsteigend
targetBreakLength = 0
reducedWorkTime = (paidWorkingTime + paidOvertime)
für jede breakSetting:
wenn reducedWorkTime ≥ breakSetting.timeLimit:
wenn timeProfile.countBreakPartially:
diff = reducedWorkTime − breakSetting.timeLimit
targetBreakLength += min(diff, breakSetting.breakLength)
sonst:
targetBreakLength += breakSetting.breakLength
reducedWorkTime -= targetBreakLength
wenn breakTime (= geleistet) ≥ targetBreakLength: correction = 0
sonst: correction = targetBreakLength − breakTime
Der correction-Wert (in Sekunden) wird von paidWorkingTime abgezogen.
Effekt von
count_break_partially:
false(Standard): Sobald die Schwelle erreicht ist, wird die vollebreakLengthverlangt. Das kann harte Sprünge an der Schwelle verursachen.true: Die Pflichtpause wächst linear mit der Überschreitung – z. B. bei „Nach 6h: 30min" und 6h05min Arbeitszeit werden nur 5min Pause verlangt; bei 6h35min die vollen 30min.
9.5 Cap auf Profilzeit + Überstunden-Split
Nach der Pausenkorrektur:
paidWorkingTime = min(profileTime, paidWorkingTime + paidOvertime)
paidOvertime = max(0, (paidWorkingTime + paidOvertime) − profileTime)
D. h. paid_working_time ist nach oben durch profile_time gedeckelt, jeglicher Überschuss landet in paid_overtime.
9.6 Unerfasste Präsenz (unAccountedPresence)
Nach Abzug aller Block-Schnitte verbleibt in einem BookingEntry ggf. „Restzeit" (getTimeLeft() > 0) – das ist Anwesenheit außerhalb jedes geplanten Blocks.
sumOfUnAccountedForPresence = Σ entry.getTimeLeft() für alle isPresent=true Einträge
Der Endpunkt approveUnAccountedPresence sendet unAccountPresenceAction='approved':
paidOvertime += sumOfUnAccountedForPresencesumOfUnAccountedForPresence = 0
declineUnAccountedPresence (Aktion 'declined'/beliebiger Wert ≠ null) verwirft die Restzeit ohne Anrechnung.
Wird nichts entschieden und unAccountedPresence > 0, sendet TimeProfileWorkNotificationProcessor::sendTimeProfileWorkerJournalNotification() eine Benachrichtigung an den Verantwortlichen.
9.7 Bezahlte Abwesenheit
getAbsenceBookingEntries() lädt alle genehmigten Absence-Einträge (Urlaub, Krankheit, Feiertag) im Tageszeitraum und erzeugt BookingEntrys mit
isPresent = (absenceCode.excused || isHoliday) && status === 'Approved'.
Anschließend wird calculatePaidTime(... onlyStandardTime=true) aufgerufen → nur Day-Lines mit calcStandardTime=true zählen → Ergebnis in paid_absence_time.
9.8 Feiertagsbehandlung (checkForHolidayWork)
- Primäre Arbeitsadresse des Mitarbeiters → Bundesland (
State). PublicHolidayfürjournal.dateundPublicHolidayStatefür dieses Bundesland prüfen.- Aktuelle
Employmentfür den Mitarbeiter laden:- Existiert
employment.isHolidayWorkAllowed? DannisAllowed = true. - Andernfalls:
isHolidayWork = true, isAllowed = false.
- Existiert
- Wenn Feiertag und nicht erlaubt:
paid_absence_timewird aufprofile_timegesetzt (kompletter Tag wird als bezahlte Abwesenheit verbucht). journal.is_holiday = truewird gesetzt.
9.9 Tagessaldo
balance = paidWorkingTime + paidAbsenceTime + paidOvertime − profileTime
Hinweis: Da
paidWorkingTime + paidOvertimebereits durch den Cap (§ 9.5) so verteilt sind, dasspaidWorkingTime ≤ profileTime, repräsentiertbalancefaktisch den Anteil über/unter Soll.
9.10 Kumulativer Saldo (balance_accumulated)
Implementiert in TimeProfileWorkerJournalBalanceCalculationProcessor::calculateBalances():
- Suche letztes genehmigtes Journal vor
journal.datedesselben Mitarbeiters →previousJournal.balance_accumulatedals Startwert. - Sammle dieses Journal + alle nachfolgenden genehmigten Journale (
findNextApprovedJournals). - Pro Journal:
- Lade alle
BalanceCorrection-Datensätze für(worker, date)und summiere →balanceCorrection. accumulatedBalance += balanceCorrection + journal.balance- Persistiere
journal.balance_accumulated = accumulatedBalance.
- Lade alle
- Flush.
Der Recalc läuft also kettenweise vom geänderten Journal nach vorn – Genehmigungen oder Korrekturen invalidieren den kumulativen Saldo aller Folgetage und werden hier durchgereicht.
9.12 Berechnungsperiode (calculation_period – 0_24 vs. 12_12)
Die Property TimeProfile.calculationPeriod (Migration Version20260422143051.php, VARCHAR(255) DEFAULT NULL) bestimmt das Tagesfenster, über das ein Journal ausgewertet wird. Sie kennt zwei Ausprägungen:
| Wert | Tagesfenster | Anwendungsfall |
|---|---|---|
'0_24' (Standard) | [Datum 00:00 ; Datum 23:59:59] – kalendarischer Tag | „Normaler" Bürobetrieb |
'12_12' | Datum−1d 12:00 ; Datum 12:00) – Mittag des Vortages bis Mittag des Tages | Schicht-/Nachtbetrieb mit Schichtwechsel um 12:00 |
Migrations-/Bestand-Behandlung: Bei null verhalten sich alle Pfade wie bei '0_24'. Das Konsolen-Kommando [app:migrate-time-profile-calculation-period iteriert mandantenweise über alle TimeProfiles (außer Master-SysClient) und setzt calculationPeriod = '0_24', wo der Wert noch leer ist.
9.12.1 Auswirkung in TimeProfileWorkerJournalCalculationProcessor::calculateJournalTimes()
wenn timeProfile.calculationPeriod === '12_12':
start = Carbon(journal.date)->subDay()->setHours(12)
end = Carbon(journal.date)->setHours(12)
sonst:
start = Carbon(journal.date)->startOfDay()
end = Carbon(journal.date)->endOfDay()
bookings = TimeControlBookingRepository->findInRangeFiltered(start, end, worker, null, true)
Da das Fenster die Buchungs-Quellmenge bereits bestimmt, wirken alle nachfolgenden Phasen (Validierung, BookingEntry-Bildung, Pausen-, Saldo- und Überstundenrechnung) automatisch auf den richtigen Schicht-Zeitraum. Insbesondere können Buchungen aus zwei verschiedenen Kalendertagen demselben Journaleintrag zugerechnet werden.
9.12.2 Auswirkung in getAdjustedDate()
Die private Hilfsmethode getAdjustedDate(?DateTime $time, ?DateTime $date, TimeProfile $timeProfile) in TimeProfileWorkerJournalCalculationProcessor.php#L485 nimmt jetzt zusätzlich das TimeProfile entgegen. Sie wird verwendet, um Day-Line-Zeiten und Buchungs-Zeitstempel auf das tatsächliche Schicht-Datum zu projizieren:
$targetDateTime = $date.setTime(time.hour, time.minute, time.second)
$calculationPeriodEnd = Carbon($date)->setTime(12,0,0,0)
wenn timeProfile.calculationPeriod === '12_12'
&& $targetDateTime >= $calculationPeriodEnd:
$targetDateTime = $targetDateTime->subDay()
Effekt: Bei '12_12' werden Zeiten ab 12:00 dem Vortag zugeordnet. Damit liegen Day-Line-Start und Buchungen, die in der Vorabend-/Nachthälfte einer Mittag-zu-Mittag-Schicht stattfinden, korrekt im selben Schicht-Datum wie die Folgezeit ab 00:00 des Journal-Datums.
Diese Anpassung wirkt überall dort, wo getAdjustedDate() benutzt wird – unter anderem in:
- der Überlappungsprüfung von Buchungen mit Day-Lines (
getOverlappingBookingEntries), - der Toleranzlogik (
isWithinTolerancemitnextDayLine.toleranceBeforeStart/previousDayLine.toleranceAfterEnd), - der Pausenberechnung (
calculateTotalBreakTime,consumeUnplannedBreakTimePresence), - der Sortierung der Day-Lines vor der Iteration.
9.12.3 Auswirkung in ProfileTimeCalculationProcessor::calculateProfileTime()
Die statische Methode dedupliziert die Logik nicht über getAdjustedDate, sondern arbeitet direkt mit Carbon-Instanzen:
$endTime = Carbon(dayLine.endTime)->setDateFrom(now)
$startTime = Carbon(dayLine.startTime)->setDateFrom(now)
$calculationPeriodEnd = Carbon(now)->setTime(12,0,0,0)
wenn timeProfile.calculationPeriod === '12_12'
&& $startTime >= $calculationPeriodEnd
&& $endTime < $calculationPeriodEnd:
$startTime = $startTime->modify('-1 day')
profileTime += $endTime.timestamp - $startTime.timestamp
Damit wird der Sondersfall „Block beginnt nachmittags und endet am nächsten Vormittag" ohne negative Differenz aufgelöst.
9.12.4 Auswirkung im API-Endpoint GET /api/v2/timeProfileWorkerJournals/events/{id}
TimeProfileWorkerJournalControllerV2::getBookingsByJournal() wendet dieselbe Fenster-Logik wie der Calculation-Processor an, damit der Journal-Detail-Drawer im FE genau die Buchungen und genehmigten Abwesenheiten der zugehörigen Schicht erhält:
'12_12'→ Buchungen + Absences in[date−1d 12:00 ; date 12:00)- sonst →
[date 00:00 ; date 23:59:59]
Anschließend werden Buchungen und Abwesenheiten via usort über ihre Start-Zeitstempel zu einer einheitlichen Liste verschmolzen.
9.12.5 Auswirkung im Frontend
| Stelle | Datei | Verhalten |
|---|---|---|
TimeProfile-Modell | TimeProfile.model.ts | Neue Property calculation_period: string. Wird im getTransferObject() an das Backend zurück gesendet. Default im Konstruktor: false-Fallback (effektiv leer, wenn nicht gesetzt). Auch im TimeProfileTransfer-Interface (Zeile 66) ergänzt |
TimeProfileDialog-Form | time-profile-dialog.component.ts und .component.html | Neuer required mat-select calculation_period mit Label „Berechnung" und den Optionen „0-24 Modell" ('0_24') und „12-12 Modell" ('12_12'). Wird beim Edit aus data.timeProfile.calculation_period vorbelegt und in sendData() als timeProfile.calculation_period gesetzt |
TimeProfileDayLine Konstruktor | TimeProfileDayLine.model.ts | Bei time_profile.calculation_period === '12_12' und start_time ≥ 12:00 wird start_time als heutiger Datum-Zeitwert (Date-String) gespeichert; sonst bleibt der Original-HH:mm-String erhalten. Damit werden Day-Lines zur Sortierung als datierte Werte interpretiert |
| Day-Line-Sortierung | time-profile-day.datasource.ts#L46 | Bei '12_12' wird über new Date(start_time).getTime() sortiert, sonst über start_time_minutes. So bleibt die korrekte Schicht-Reihenfolge auch über die Mittagsgrenze hinweg erhalten |
| Journal-Detail-Header | time-profile-worker-details-v2.component.ts#L42 und .html#L155 | Properties calculationPeriod: string = '0_24' und yesterday: Date | null. Bei journal.time_profile.calculation_period === '12_12' zeigt der „Datum"-Block nicht ein, sondern zwei Daten an: gestern / heute (gestern = journal.date − 1 Tag). Sonst wird nur journal.date angezeigt |
| Buchungs-Dialog V2 | time-profile-worker-booking-dialog-v2.component.ts#L83 | Beim Speichern manueller Buchungen aus dem Journal-Detail wird das entry_date_time rück-datiert, falls calculation_period === '12_12' und die eingegebene Uhrzeit ≥ 12:00 ist – targetDate = journal.date − 1 Tag, sonst targetDate = journal.date. Damit landen Vorabend-Stempelungen auf dem korrekten Kalendertag, gehören aber dem geöffneten Journal an |
| Filter-Typ | time-profile-worker-types.ts | TimeProfile-Interface erhält calculation_period?: string |
9.12.6 Auswirkungen auf Folge-Berechnungen
- Pausenberechnung: Da
getAdjustedDate()für'12_12'Day-Line-Zeiten ≥ 12:00 auf den Vortag projiziert, schneiden sich Buchungen und Day-Lines korrekt über die Mittagsgrenze hinweg. Pausen-Töpfe (totalBreakPlanned,totalBreakNotPlanned) verhalten sich identisch zur 0_24-Variante – nur das Tagesfenster ist verschoben. - Toleranzen: Die
toleranceBeforeStart-/toleranceAfterEnd-Logik inisWithinTolerance()operiert auf den pergetAdjustedDate()projizierten Zeiten und benötigt deshalb keine separate Sonderbehandlung. - Saldo:
balanceundbalance_accumulatedsind weiterhin in Sekunden ausgedrückt; nur die Datenbasis (Buchungen pro Tagesfenster) verschiebt sich. Der kettenweise Recalc in TimeProfileWorkerJournalBalanceCalculationProcessor::calculateBalances() bleibt unverändert. - Feiertagsbehandlung:
checkForHolidayWork()arbeitet weiterhin mitjournal.dateals Referenztag – ein Feiertag wird der Schicht zugeordnet, deren Journal-Datum der Feiertag ist (auch wenn die Schicht real schon am Vortagmittag begonnen hat).
9.11 Berechnungs-Trigger
Die Methode setzt am Ende:
is_calculated = !hasErrorsis_approved = !hasErrorsun_accounted_presence = sumOfUnAccountedForPresencestart_time = bookings[0].startDate,end_time = bookings[last].endDate
und ruft balanceCalculator->calculateBalances($journal) nur, wenn keine Fehler vorliegen.
10. Lifecycle eines Journals
Cron: CreateTimeProfileWorkJournalsCommand
│
▼
┌───────────────────┐
│ Journal angelegt │ status=null, is_calculated=false, is_approved=false
└──────────┬────────┘
│
TimeControlBooking-Stempelungen entstehen tagsüber
│
Cron: CalculateTimeProfileJournalsCommand (oder API /calculate)
│
▼
┌───────────────────┐
│ Berechnung läuft │ TimeProfileWorkerJournalCalculationProcessor
└──────────┬────────┘
│
┌────────┴───────┐
▼ ▼
hasErrors=true hasErrors=false
│ │
▼ ▼
status='invalid' status='valid'
isCalculated=false isCalculated=true
isApproved=false isApproved=true
errorDesc set Saldo-Recalc folgt
│
▼
TimeProfileJournalListener::preUpdate()
│
▼
Notification an Vorgesetzten
│
▼
FE: Manuelle Korrektur / approve / Neuberechnung
Vom Frontend aus existieren drei Eingriffspunkte:
- „Berechnen" –
GET /api/timeprofileworkerjournals/{id}/calculate - „Genehmigen" – Toggle
is_approvedperPUT /api/timeprofileworkerjournals/{id} - „Unerfasste Präsenz" – Aktion
approvebzw.decline(siehe § 9.6); der Backend-Parameter heißtunAccountPresenceAction
11. API-Endpunkte
11.1 Arbeitszeitsjournale (V1) – TimeProfileWorkerJournalController.php
| Methode | Pfad | Zweck |
|---|---|---|
| GET | /api/timeprofileworkerjournals | Listing (paginiert, gefiltert) |
| GET | /api/find/timeprofileworkerjournals | Suche |
| GET | /api/timeprofileworkerjournals/{id} | Einzelnes Journal |
| POST | /api/timeprofileworkerjournals | Anlegen |
| PUT | /api/timeprofileworkerjournals/{id} | Aktualisieren (z. B. is_approved) |
| DELETE | /api/timeprofileworkerjournals/{id} | Löschen |
| GET | /api/timeprofileworkerjournals/{id}/calculate | Manuelle Neuberechnung |
| GET | /api/getlatesttimeprofileworkerjournals/{workerId} | Letzte Journale eines Mitarbeiters |
| GET | /api/mytimeprofilejournals | Eigene Journale |
| GET | /api/latestjournalsforcurrentuser | Letzte eigene |
| GET | /api/timeprofileworkerjournalsWithCorrections | Inkl. BalanceCorrections |
| GET | /api/timeProfileWorkerJournalsDataForDay | Tagesdaten |
| GET | /api/timeProfileWorkerJournalsDataForWeek | Wochendaten |
11.2 Arbeitszeitsjournale (V2) – TimeProfileWorkerJournalControllerV2.php
| Methode | Pfad |
|---|---|
| GET | /api/v2/timeProfileWorkerJournals |
| GET | /api/v2/find/timeProfileWorkerJournals |
| GET | /api/v2/timeProfileWorkerJournals/{id}/thumbnail |
| GET | /api/v2/timeProfileWorkerJournals/events/{id} (Buchungen + Abwesenheiten für Detailansicht) |
11.3 Stempelbuchungen
Siehe § 5.5. Die wichtigsten:
| Methode | Pfad | Zweck |
|---|---|---|
| GET/POST/PUT/DELETE | /api/timecontrolbookings[/{id}] | CRUD Stempelungen (mit ?admin=true für manuelle Anlage durch HR/Admin) |
| POST | /api/timeControlBookingForce | Anlegen ohne Duplikat-/Matching-Prüfung |
| GET | /api/getcurrentbookingstatus | Aktueller Status des Users |
| GET | /api/getRawBookingTime | Verstrichene Zeit |
| GET | /api/getmappedbookingtypes | Kategorisierte Buchungstypen für Terminal-UI |
| GET/POST/PUT/DELETE | /api/timecontrolbookingtypes[/{id}] | CRUD Buchungstypen |
| GET/PUT | /api/switchcheckinbookingtype/{id} | Toggle isCheckIn |
| GET/PUT | /api/switchcheckoutbookingtype/{id} | Toggle isCheckOut (exklusiv) |
| GET/POST/PUT/DELETE | /api/timeControlBookingRequest[/{id}] | Antragsworkflow für nachträgliche Buchungen |
11.4 Konfigurations-Controller
TimeProfileController–/api/timeprofilesTimeProfileDayController,TimeProfileDayLineControllerTimeProfileTypeControllerTimeProfileBreakSettingControllerTimeProfileGroupController,TimeProfileGroupWorkerControllerTimeProfileCalendarControllerTimeProfileWorkerJournalReportController– Berichte
12. Berechtigungen
Die Berechtigungsprüfung erfolgt dreistufig:
- Route-Guards (
canActivate) verhindern die Navigation zu nicht erlaubten Routen. - Komponenten-Methoden (z. B.
actionsAllowed()) blenden UI-Aktionen aus, wenn die Permission fehlt oder die Logik (Eigentümer-Check) nicht erfüllt ist. - Backend-Enforcement (Voters /
#[IsGranted]) – Pflichtschicht. FE-Checks sind nur UX-Convenience.
12.1 Infrastruktur
Guard requirePermission(...permissions) – src/app/guards/permissions.guard.ts
export const requirePermission = (...permissions: Permission[]): CanActivateFn => {
return async (route, state) => {
const userService = inject(CurrentUserService);
const snackBar = inject(MatSnackBar, { optional: true });
const user = await userService.getUser();
const userPermissions = user.roles.flatMap(r => r.perms).map(p => p.name);
for (const p of permissions) {
if (!userPermissions.includes(p)) {
snackBar?.open('Zugriff verweigert', 'Ok', { duration: 6000 });
return false;
}
}
return true;
};
};
- Variadisch: mehrere Permissions = AND-Verknüpfung (Beispiel
/timecontrol/workbenötigtprojects_READUNDtimecontrol_user). - Fehler-UX: MatSnackBar „Zugriff verweigert", Navigation wird abgebrochen.
- Der TS-Typ
Permission(~200 Keys) liefert IDE-Autocomplete und verhindert Typos.
Permission-Lookup userHasPermission(user, name) – src/app/utils/permission-helpers.ts
const userHasPermission = (user, permission) =>
!!user.roles.find(r => r.perms.find(p => p.name === permission));
Wird aus Komponenten-Methoden heraus aufgerufen (s. u. actionsAllowed()).
CurrentUserService – src/app/Services/user-service.service.ts
- Singleton (
providedIn: 'root'). getUser(): Promise<User>lädt einmalig perPOST /auth/meund cached das User-Objekt im Memory.- Response-Shape:
{ user: { id, name, worker, roles: [{ id, name, perms: [{ id, name }] }], user_settings, ... } }. onUserChange: BehaviorSubject<User>für reaktive Updates in Komponenten.- Kein LocalStorage/SessionStorage – Permissions werden bei jedem Reload frisch geladen (verhindert Stale-Cache).
- Keine Custom-Direktive (
*hasPermission) – Sichtbarkeit wird über*ngIf="<methode>()"gesteuert.
12.2 Permission-Detail: time_profile_journal_access
| Aspekt | Details |
|---|---|
| Backend | PermissionEnum::TimeProfileJournalAccess, beschrieben mit „Access to the time profile worker journals" |
| FE-Verwendung | Route-Guard auf timecontrol.module.ts für time-profile-worker-journals; lazy-loaded TimeProfileWorkerV2Module |
| Schaltet frei | – Listenseite mit Filter (Worker, Datumsbereich, Geschäftsbereich, Abteilung, Team, Standort, Status) – Detail-Drawer eines Journals (read-only) – Aufrufe GET /api/v2/timeProfileWorkerJournals, GET /api/v2/find/timeProfileWorkerJournals, GET /api/timeprofileworkerjournals/{id}, GET /api/v2/timeProfileWorkerJournals/events/{id} |
| Wichtiger Hinweis | Lese-Zugriff. Bearbeiten/Genehmigen erfordert zusätzlich time_profile_journal_edit_all oder time_profile_journal_edit_others |
12.3 Permission-Detail: time_profile_journal_edit_all
| Aspekt | Details |
|---|---|
| Backend | PermissionEnum::TimeProfileJournalEditAll – „Allows user to edit all time profile journals" |
| FE-Verwendung | In actionsAllowed() der Komponenten – TimeProfileWorkerDetailsV2Component – TimeProfileWorkerBookingItemV2Component |
| Schaltet frei | – „check_circle"-Icon (Genehmigen-Toggle) – „calculate"-Button (manuelle Neuberechnung) – „check_circle"/„cancel"-Buttons für unerfasste Präsenz – „add"-Button „Zeiten hinzufügen" → öffnet TimeProfileWorkerBookingDialogV2Component – Edit-/Delete-Icons je TimeControlBooking-Zeile (im …BookingItemV2) |
| API-Aufrufe | – PUT /api/timeprofileworkerjournals/{id} (is_approved, status) – GET /api/timeprofileworkerjournals/{id}/calculate[?unAccountPresenceAction=approved|declined] – POST /api/timecontrolbookings?admin=true – PUT /api/timecontrolbookings/{id}?admin=true – DELETE /api/timecontrolbookings/{id} |
12.4 Permission-Detail: time_profile_journal_edit_others
| Aspekt | Details |
|---|---|
| Backend | PermissionEnum::TimeProfileJournalEditOthers – „Allows user to edit all time profile journals except for their own" |
| FE-Verwendung | Kombination im selben actionsAllowed() |
| Logik | userHasPermission(user, 'time_profile_journal_edit_all') ‖ (userHasPermission(user, 'time_profile_journal_edit_others') ∧ journal.workerid !== currentWorker.workerid) |
| Schaltet frei | Gleiche Aktionen wie edit_all, aber nur wenn das geöffnete Journal nicht das eigene ist. Ohne edit_all ist keine Selbst-Bearbeitung möglich – verhindert Manipulation des eigenen Saldos |
| API-Aufrufe | wie edit_all, server-seitig spiegelbildlich gegen den Worker-Owner geprüft |
12.5 Permission-Detail: timecontrol_user
| Aspekt | Details |
|---|---|
| Backend | PermissionEnum::TimeControlUser – „can access time control module" |
| FE-Verwendung | Route-Guard an mindestens 7 Stellen: – /apps/timecontrol (Wurzel; apps.module.ts) – /apps/timecontrol/main – /apps/timecontrol/work (kombiniert mit projects_READ) – /apps/timecontrol/invoice – /apps/time-control-dashboard und …-simple – /apps/time-profile-reports |
| Schaltet frei | – Stempel-UI (TimeControlComponent – Check-In/Check-Out Buttons, Standortwahl, Pause-Buttons, Industrie-Timer) – Auftragszeit-Modul (TimeControlWorkComponent) – Rechnungs-Generierung aus Buchungen (InvoiceModule) – TimeControl- und Balance-Dashboards – Reports unter /apps/time-profile-reports |
| API-Aufrufe | Alle /api/timecontrolbookings* POST/PUT/GET, /api/getcurrentbookingstatus, /api/getRawBookingTime, /api/getmappedbookingtypes, sämtliche Auftragszeit-Endpunkte |
12.6 Permission-Detail: projects_READ
| Aspekt | Details |
|---|---|
| Backend | PermissionEnum::ProjectsRead – „display a project" |
| FE-Verwendung | Route-Guard auf /apps/timecontrol/work, kombiniert mit timecontrol_user |
| Schaltet frei | – Auswahl/Filter von Projekten/Tasks/Cases im TimeControlWorkComponent – Zeitbuchungen gegen Projekte (TimeControlWorkBooking) |
12.7 Permission-Detail: timecontrol_settings
| Aspekt | Details |
|---|---|
| Backend | PermissionEnum::TimeControlAdmin = 'timecontrol_settings' – „Has access to the settings time control module" |
| FE-Verwendung | Route-Guards der Settings-Submodule: – /apps/timecontrol/settings/time-profiles – …/time-profile-types – …/time-control-basic-settings – …/timechips (und WorkerChips) – …/time-profile-groups – …/time-profile-breaks – …/time-profile-calendar – …/time-control-work-settings |
| Schaltet frei | Konfiguration aller Stamm- und Konfigurationsdaten des Features (Profile, Profiltypen, Gruppen, Pausen, Kalender, Buchungstypen, Karten) |
12.8 Permission-Detail: timecontrol_journals
| Aspekt | Details |
|---|---|
| Backend | PermissionEnum::TimeControlJournals – „Has access to the journals time control module" |
| FE-Verwendung | Route-Guard auf /apps/timecontrol/settings/journals (lazy TimeProfileJournalModule) |
| Schaltet frei | Erweiterte Journal-Konfiguration (über die Standard-Settings hinaus) |
12.9 Permission-Detail: timecontrol_shift_planner
| Aspekt | Details |
|---|---|
| Backend | PermissionEnum::TimeControlShiftPlanner – „can access shift planner" |
| FE-Verwendung | Route-Guard auf /apps/holidayPlanner/shift-planner (calendar.module.ts) |
| Schaltet frei | Schichtplaner-Werkzeug innerhalb des Holiday-Planner-Moduls |
12.10 Permission-Detail: timecontrol_re_registration_other & timecontrol_re_registration_date
| Aspekt | Details |
|---|---|
| Backend | PermissionEnum::TimecontrolReRegistrationOther und …RegistrationDate |
| FE-Verwendung | Eigene Guards timecontrolReRegistrationWorker.guard.ts und timecontrolReRegistrationDates.guard.ts im Re-Registration-Workflow (TimeControlBookingRequest) |
| Schaltet frei | – _other: Stempelantrag für andere Mitarbeiter anlegen – _date: Stempelantrag für andere Datumswerte (rückwirkend) anlegen |
12.11 Permission-Detail: Superset-Dashboards
Alle vier folgen dem gleichen Muster: Route-Guard kombiniert hasSupersetConfig(<superset-url>) und requirePermission(...).
| Permission | Route | Komponente |
|---|---|---|
superset_time_journal_report_read | /apps/time-journal-report | TimeJournalDashboardComponent |
superset_working_time_report | /apps/working-time-report | SupersetWorkingTimeReportComponent |
superset_salary_dashboard_read | /apps/salary-dashboard | SalaryDashboardComponent |
superset_time_control_booking_location_report | /apps/hrm/booking-location-report | BookingLocationReportComponent |
Die FE-Permission gibt nur den Eintritt zum Embed frei – die Daten-Sicht innerhalb von Apache Superset wird über das Superset-eigene RBAC nochmals abgesichert.
12.12 Permission-Detail: create_salary_elements
| Aspekt | Details |
|---|---|
| Backend | PermissionEnum::CreateSalaryElements – „Permission to create new salary elements" |
| FE-Verwendung | In den Salary-Verwaltungskomponenten (Dialog-Open-Logik, „Neu"-Buttons) |
| Schaltet frei | Anlage neuer Lohnarten (relevant für die Schritt-3-Schalter aus § 8.4 zur Lohnauswertung) |
12.13 Verwandte Permissions im Umfeld
Diese werden in benachbarten Modulen (Absenz, Personal, Dashboards) verwendet, beeinflussen aber Inhalte, die im Journal sichtbar sind:
| Permission | Wirkung in Journal-Kontext |
|---|---|
approve_TIMEPROFILEJOURNALS, supervisor_hr_clerk_journals, hr_administrator_journals | Genehmigungs-Workflow durch verschiedene Rollen |
workers_READ | Voraussetzung, um Worker-Daten in Filtern und Listen zu laden |
all_ABSENCES | Sichtbarkeit von Abwesenheits-Einträgen im Journal-Detail |
holiday_planner_time_profile_change_others / _all | Tagesgenauer Wechsel des Zeitprofils anderer/aller Mitarbeiter aus dem Holiday-Planner |
presence_dashboard_my_all_filter, presence_dashboard_absence_registration | Anwesenheits-Dashboard-Filter und Abwesenheits-Anlage |
balance_dashboard_access | Saldo-Dashboard-Zugriff |
absence_creation_vacation_lock_bypass, vacation_edit_details | Sonderfälle bei Urlaubssperre |
12.14 Permission-Matrix (Feature → Permission → Route → API)
| Feature | Permission(s) | FE-Eintrittspunkt | Sichtbarkeits-/Aktivierungslogik | Backend-Endpoints |
|---|---|---|---|---|
| Journale ansehen | time_profile_journal_access | /timecontrol/time-profile-worker-journals | requirePermission Route-Guard | GET /api/v2/timeProfileWorkerJournals, …/find, …/{id}/thumbnail, …/events/{id}, GET /api/timeprofileworkerjournals/{id} |
| Journal genehmigen / berechnen | time_profile_journal_edit_all ‖ (time_profile_journal_edit_others ∧ Fremd-Journal) | Selbe Route + actionsAllowed() | Methodischer Check + *ngIf | PUT /api/timeprofileworkerjournals/{id}, GET /api/timeprofileworkerjournals/{id}/calculate |
| Stempelung im Journal hinzufügen / ändern / löschen | gleich wie oben | Buttons im Detail/Item | actionsAllowed() | POST /api/timecontrolbookings?admin=true, PUT /…?admin=true, DELETE /api/timecontrolbookings/{id} |
| Eigene Stempelung am Terminal | timecontrol_user | /timecontrol/main | Route-Guard | GET /api/getmappedbookingtypes, GET /api/getcurrentbookingstatus, GET /api/getRawBookingTime, POST /api/timecontrolbookings |
| Auftragszeitbuchung | timecontrol_user ∧ projects_READ | /timecontrol/work | Beide Route-Guards (AND) | /api/timecontrolworkbookings*, Project-Filter-Endpoints |
| Rechnungen aus Zeitbuchungen | timecontrol_user | /timecontrol/invoice | Route-Guard | Invoice-Endpoints |
| Stammdaten konfigurieren | timecontrol_settings | /timecontrol/settings/* | Route-Guard pro Sub-Route | CRUD /api/timeprofiles, /api/timeprofiletypes, /api/timeprofilegroups, /api/timecontrolbookingtypes, /api/timechips, /api/workerchips |
| Journal-Konfiguration | timecontrol_journals | /timecontrol/settings/journals | Route-Guard | Journal-Settings-Endpoints |
| Schichtplaner | timecontrol_shift_planner | /holidayPlanner/shift-planner | Route-Guard | Shift-Planner-Endpoints |
| Nach-Stempel-Antrag (für andere/datum) | timecontrol_re_registration_other / …_date | Re-Registration-Komponenten | Eigene Guards | /api/timeControlBookingRequest* |
| BI Time-Journal | superset_time_journal_report_read | /time-journal-report | hasSupersetConfig ∧ requirePermission | Superset Embed |
| BI Working Time | superset_working_time_report | /working-time-report | dito | Superset Embed |
| BI Salary | superset_salary_dashboard_read | /salary-dashboard | dito | Superset Embed |
| BI Booking Location (HRM) | superset_time_control_booking_location_report | /hrm/booking-location-report | dito | Superset Embed |
| Lohnarten anlegen | create_salary_elements | Salary-Modul | Komponenten-Check | Salary-CRUD |
12.15 Sicherheits-Hinweise
actionsAllowed()prüft im selben Aufruf beide Edit-Permissions sowie den Worker-Owner-Vergleich. Wer eine Permission ohne den anderen Rollen-Eintrag erhält, kann genau die UI-Aktion sehen, die die Permission deckt.- Der
?admin=true-Query-Parameter beim manuellen Anlegen/Bearbeiten vonTimeControlBookings aus dem Journal-Detail ist nur ein FE-Hinweis – die tatsächliche Autorisierung passiert serverseitig (Voter /#[IsGranted]imTimeControlBookingController). - Für
time_profile_journal_edit_othersfindet die Owner-Prüfung client- und serverseitig statt; das Backend muss dieselbe Logik enforcen, da der FE-Check rein UX ist. - Bei Superset-Routen genügt die Backend-Permission allein nicht – sowohl die FE-Konfiguration (
hasSupersetConfig) als auch die Superset-interne RBAC müssen den User zulassen.
13. Reports & Dashboards
Komponenten
- time-profile-reports.component – Report-Hub
monthly-overview-report-dialog.component– Monatsübersicht (month,year,businessArea)report-by-business-area.component– Aggregat nach Geschäftsbereichtime-control-booking-report.component– Detail-Buchungsreport (PDF viaTimeControlBookingReportProcessor)
Dashboards
time-journal-dashboard.componentsuperset-balance-dashboard.component– BI-Dashboard Saldo (Apache Superset Embed)superset-working-time-report.component– BI-Dashboard Arbeitszeitsalary-dashboard.componentbooking-location-report.component(HRM-Modul)
Backend
TimeProfileWorkerJournalReportControllerTimeControlBookingReportController–GET /api/reports/timecontrolbookingTimeProfileWorkerJournalReportProcessorTimeProfileWorkerJournalWeekReportProcessor,WeekData,WorkerDataTimeBookingDashboard– Aggregations-Processor für Anwesenheits-Dashboards (getTimeControlStatus,dashboard/numbers)
14. Console Commands & geplante Jobs
Alle unter bin/console:
| Command | Klasse | Zweck |
|---|---|---|
app:create-time-profile-work-journals | CreateTimeProfileWorkJournalsCommand | Tägliche Anlage leerer Journale für aktive Mitarbeiter |
app:calculate-time-profile-journals | CalculateTimeProfileJournalsCommand | Batch-Berechnung offener Journale |
app:calculate-time-profile-times | CalculateTimeProfileTimesCommand | Profilzeit-Vorberechnung |
app:time-profile-calendar-entry-generation | TimeProfileCalendarEntryGenerationCommand | Auto-Generierung von TimeProfileCalendar-Einträgen pro Gruppe |
app:generate-time-profile-worker-journal-evaluation | GenerateTimeProfileWorkerJournalEvaluationCommand | Evaluation-Datensätze für Reports/Lohn |
app:delete-duplicate-time-profile-journals | DeleteDuplicateTimeProfileJournalsCommand | Bereinigung doppelter Journale |
app:create-test-time-control-bookings <worker_id> | CreateTestTimeControlBookings | Testdaten für Stempelungen (Mo–Fr, 8:00–17:00) |
app:time-profile-group-worker-migration | TimeProfileGroupWorkerMigrationCommand | Datenmigration Gruppen↔Mitarbeiter |
app:time-profile-and-salary-syclient-migration | TimeProfileAndSalarySyClientMigrationCommand | Multi-Tenant-Migration |
app:create-test-time-profile-calendar-entries | CreateTestTimeProfileCalendarEntriesCommand | Testdaten Kalender |
app:time-control-bookings-migrate | TimeControlBookingsMigrateCommand | Datenmigration Stempelungen |
app:migrate-time-profile-calculation-period | TimeProfileCalculationPeriodCommand | Migriert pro SysClient (außer Master) alle TimeProfile.calculation_period-Werte von null auf '0_24'. Einmalig nach Migration Version20260422143051.php auszuführen |
pcsconnect:poll:timebookings <sysClient> | PollForNewTimeControlBookingsCommand | Pollt externes INTUSCOM-Terminalsystem auf neue Stempelungen |
Anhang A – Schalter-Cheatsheet (Wirkung auf Berechnungs-Töpfe)
Welcher Topf eines Tagesblocks (TimeProfileDayLine) durch welches Flag des TimeProfileType befüllt wird:
| Flag | paid_working_time | paid_overtime | unpaid_absence_time | paid_absence_time | Pausen-Töpfe | Profilzeit (Soll) |
|---|---|---|---|---|---|---|
calc_standard_time | ✓ | – | – | ✓ (über Absence-Pass) | – | ✓ |
calc_rewarded_time | (nur Pass-Filter) | – | – | – | – | – |
calc_paid_overtime | – | ✓ | – | – | – | – |
calc_flexible_working_hours_plus | – | ✓ | – | – | – | – |
calc_flexible_working_hours_minus | – | – | – (negativ) | – | – | – |
calc_justified_absence | – | – | – | – | totalBreakPlanned, totalPlannedWithinPresence | – |
calc_unauthorized_absence | – | – | ✓ | – | – | – |
Profil- und Buchungstyp-Schalter:
| Schalter | Wirkung |
|---|---|
time_profile.is_fallback | Default-Profil bei fehlender Zuweisung |
time_profile.count_break_partially | Pflichtpause anteilig statt Block |
time_profile.calculation_period | '0_24' (Standard-Tag) oder '12_12' (Mittag-zu-Mittag-Schicht) – steuert Tagesfenster aller Berechnungen, FE-Sortierung der Day-Lines, Anzeige zweier Daten im Journal-Header und Vortags-Rückdatierung manueller Buchungen |
time_profile_group.auto_generate_calendar_entries | Auto-Erzeugung tagesgenauer Calendar-Einträge |
time_profile_group.allowed_week_days | Auf welche Wochentage auto_generate wirkt |
time_control_booking_type.is_on_site | Erzwingt location im Buchungs-Dialog |
time_control_booking_type.is_available_in_terminal_mode | Sichtbar im Stempel-Terminal |
time_control_booking_type.is_check_in / is_check_out | Steuert Reihenfolgevalidierung |
Anhang B – Kritische Stellen / Known Issues
TimeProfileWorkerJournalCalculationProcessor::calculatePaidTime()enthält ein// FIXME: Check for tolerance after start and before end– aktuell werdentolerance_after_startundtolerance_before_endnicht ausgewertet, nurtolerance_before_start(über die nächste DayLine) undtolerance_after_end(über die vorige DayLine).ProfileTimeCalculationProcessor::calculateProfileTime()enthält ein// TODO: Figure out if only checking this flag is fine. We might have to check isCalcRewardedTime as well.- Pausen-Korrektur ohne
BreakSettingsenthält auskommentierte Variante (Block 109–111 inTimeProfileWorkerJournalCalculationProcessor); aktiv ist die Variante übertotalPlannedWithinPresence. TimeProfileType.calc_flexible_working_hours_minusist im Datenmodell vorhanden, wird im Berechnungsprozessor jedoch derzeit nicht aktiv ausgewertet (außer als Klassifikator) – ggf. später ergänzen.- Statusübergang
valid↔invalidtriggert ausschließlich aufpreUpdatevon Doctrine; bei direkten DQL-Updates wird der Listener nicht aufgerufen. - Der
?admin=true-Query-Parameter beim Anlegen/Ändern vonTimeControlBookings aus dem Journal-Detail ist nur eine FE-Konvention; die echte Autorisierung muss serverseitig erfolgen. TimeProfile.calculation_periodist DB-seitignullable(VARCHAR(255) DEFAULT NULL), das FE-Form-Feld ist jedochrequiredmit nur zwei zulässigen Werten. Bestandsdaten müssen mitapp:migrate-time-profile-calculation-periodauf'0_24'migriert werden, sonst können Detaildialoge beim Speichern eine Validierungs-Fehlermeldung erzeugen. Der Default-Konstruktor in TimeProfile.model.ts belegt das Feld mit dem Falsy-Fallbackfalse, was beim Edit eines Alt-Profils ohne Period zu einem leeren Select-Wert führt.ProfileTimeCalculationProcessor::calculateProfileTime()undgetAdjustedDate()implementieren die'12_12'-Sonderlogik unabhängig voneinander. Beide verwenden den Vergleich≥ 12:00, behandeln den Grenzfall „Block startet exakt 12:00" aber unterschiedlich (incalculateProfileTimezählt der Block ohne Tagesverschiebung, ingetAdjustedDatewird er auf den Vortag projiziert). Bei der Konfiguration von Day-Lines genau auf der Mittagsgrenze ist Vorsicht geboten.
Ende der Dokumentation.