Zum Hauptinhalt springen
Version: Aktuellste Version

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​

  1. Übersicht
  2. Begriffe & Domänenmodell
  3. Architektur
  4. Datenmodell (Entities)
  5. TimeControlBooking – Stempelbuchungen
  6. Backend-Komponenten
  7. Frontend-Komponenten
  8. Konfiguration & alle FE-Einstellungen / Schalter
  9. Berechnungslogik im Detail
  10. Lifecycle eines Journals
  11. API-Endpunkte
  12. Berechtigungen
  13. Reports & Dashboards
  14. 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​

DeutschEnglisch / KlassennameBedeutung
ArbeitszeitmodellTimeProfileVorlage für Arbeitszeiten (z. B. „8h Standard", „6h Gleitzeit")
Tag (im Modell)TimeProfileDayZuordnung eines Modells zu einem Wochentag (Mo–So)
TagesblockTimeProfileDayLineEinzelner Zeitblock innerhalb eines Tages mit Start/Ende und Toleranzen
ZeitprofiltypTimeProfileTypeKlassifikation eines Tagesblocks (Standardzeit, Überstunden, Pause, Gleitzeit, …) – steuert die Berechnung
Schichtplan / ZeitmodellgruppeTimeProfileGroupGruppe von Mitarbeitern, die ein gemeinsames Arbeitszeitmodell teilen
ZeitmodellkalenderTimeProfileCalendarTagesgenauer Kalendereintrag, der einem Mitarbeiter ein konkretes TimeProfile zuweist
Pausen-EinstellungTimeProfileBreakSettingSchwellenwert-basierte Pflichtpausenregel pro TimeProfile
StempelungTimeControlBookingEinzelnes Stempel-Ereignis (Check-In/Out, Pause) mit Zeitstempel und ggf. GPS
BuchungstypTimeControlBookingTypeTyp einer Stempelbuchung (Check-In, Check-Out, Pause, …)
BuchungsantragTimeControlBookingRequestAntrag auf nachträgliche Stempelung (Workflow mit Genehmigung)
ArbeitszeitsjournalTimeProfileWorkerJournalTagesjournal pro Mitarbeiter mit Berechnungsergebnissen
BewertungseintragTimeProfileWorkerJournalEvaluationDetaildatenzeilen pro Journal (Aufteilung nach Standort/Buchungstyp)
SaldokorrekturBalanceCorrectionManuelle 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:

  1. Mitarbeiter stempelt → ein neuer TimeControlBooking wird erzeugt (über die Terminal-/Stempel-UI /timecontrol/main oder über externes Polling, siehe § 14).
  2. Nightly Job (CreateTimeProfileWorkJournalsCommand) erzeugt für jeden aktiven Mitarbeiter ein leeres TimeProfileWorkerJournal für den Tag, das Profil wird aus TimeProfileCalendar (oder TimeProfileGroup → Fallback TimeProfile.is_fallback) ermittelt.
  3. CalculateTimeProfileJournalsCommand ruft TimeProfileWorkerJournalCalculationProcessor::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.
  4. 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.
  5. 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​

FeldTypBedeutung
idintPK
namestringAnzeigename des Modells
descriptionstringFreitext
colorstringFarbe für UI-Darstellung
profileTimeint (Sekunden, optional)Optionale Gesamt-Sollzeit (üblicherweise aus Day Lines berechnet)
isFallbackboolFalls true, wird dieses Profil verwendet, wenn ein Mitarbeiter kein spezifisch zugewiesenes Profil hat
countBreakPartiallyboolSteuert, ob Pflichtpausen anteilig oder als Block abgezogen werden (siehe § 9.4)
calculationPeriodstring ('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'
salaryAgreementManyToOne SalaryAgreementVerknüpfter Lohntarif
timeProfileDaysOneToMany TimeProfileDayWochentagsblöcke
timeProfileBreakSettingsOneToMany TimeProfileBreakSettingSchwellenwert-Regeln für Pflichtpausen

4.2 TimeProfileDay – src/Entity/TimeProfileDay.php​

FeldTypBedeutung
idintPK
namestringmonday, tuesday, …, sunday
timeProfileManyToOneZugehöriges Modell
timeProfileDayLinesOneToManyTagesblöcke

4.3 TimeProfileDayLine – src/Entity/TimeProfileDayLine.php​

FeldTypBedeutung
idintPK
startTimeDateTime (HH:mm)Block-Beginn
endTimeDateTime (HH:mm)Block-Ende
toleranceBeforeStartint (Min.)Erlaubte Stempel-Frühstempelung vor Blockbeginn
toleranceAfterStartint (Min.)Erlaubte verspätete Stempelung nach Blockbeginn
toleranceBeforeEndint (Min.)Erlaubte vorzeitige Auschstempelung
toleranceAfterEndint (Min.)Erlaubte verspätete Ausstempelung
timeProfileTypeManyToOne TimeProfileTypeSteuert, wie der Block bewertet wird
timeProfileDayManyToOneZugehö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:

FeldTypSteuert
idintPK
namestringAnzeigename
codestringKlassifikator: StandardTime, Overtime, Break, PaidBreak, FlexibleWorkingHours+, FlexibleWorkingHours-, CheckIn, CheckOut
calcStandardTimeboolEinbeziehen in paid_working_time (Standard-Soll)
calcRewardedTimeboolBlock ist als „entlohnte Zeit" zu zählen
calcPaidOvertimeboolEinbeziehen in paid_overtime
calcJustifiedAbsenceboolBlock ist „berechtigte Abwesenheit" / geplante Pause
calcUnauthorizedAbsenceboolBlock ist „unberechtigte Abwesenheit"
calcFlexibleWorkingHoursPlusboolPositive Gleitzeitminuten
calcFlexibleWorkingHoursMinusboolNegative Gleitzeitminuten

Diese Flags werden in TimeProfileWorkerJournalCalculationProcessor direkt abgefragt – ihr Verhalten ist in § 9 erläutert.

4.5 TimeProfileBreakSetting – src/Entity/TimeProfileBreakSetting.php​

FeldTypBedeutung
idintPK
timeLimitint (Sek.)Schwelle: Wenn die Arbeitszeit diesen Wert übersteigt, wird …
breakLengthint (Sek.)… breakLength an Pausenzeit verpflichtend
timeProfileManyToOneZugehöriges Modell

Mehrere Regeln werden kumuliert (siehe § 9.4).

4.6 TimeProfileGroup – src/Entity/TimeProfileGroup.php​

FeldTypBedeutung
idintPK
name, descriptionstringAnzeigedaten
timeProfileManyToOneModell, das auf alle Mitglieder angewendet wird
autoGenerateCalendarEntriesboolWenn true, werden über TimeProfileCalendarEntryGenerationProcessor automatisch TimeProfileCalendar-Einträge erzeugt
allowedWeekDaysjson/arrayAuswahl 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​

FeldTypBedeutung
idintPK
datedateKonkretes Kalenderdatum
workerManyToOneMitarbeiter
timeProfileManyToOneFür diesen Tag gültiges Modell (Override gegenüber Gruppen-Default)
timeProfileGroupManyToOne (optional)Quelle, falls automatisch generiert

4.9 TimeProfileWorkerJournal – src/Entity/TimeProfileWorkerJournal.php​

Die zentrale Entity des Features.

FeldTypBedeutung
idintPK
workerManyToOneMitarbeiter
timeProfileManyToOneTagsgenau gültiges Modell
datedateTagesdatum
startTimedatetimeErster Check-In des Tages
endTimedatetimeLetzter Check-Out des Tages
paidWorkingTimeint (Sek.)Tatsächlich entlohnte Arbeitszeit (max. = profileTime)
paidOvertimeint (Sek.)Entlohnte Überstunden (Anteil über profileTime)
paidAbsenceTimeint (Sek.)Bezahlte Abwesenheit (Urlaub, Krankheit, Feiertag)
unpaidAbsenceTimeint (Sek.)Unbezahlte Abwesenheit
profileTimeint (Sek.)Soll-Arbeitszeit für den Tag
balanceint (Sek.)Tagessaldo: paidWorkingTime + paidAbsenceTime + paidOvertime − profileTime
balanceAccumulatedint (Sek.)Kumulativer Saldo inkl. aller vorhergehenden, genehmigten Journale + BalanceCorrections
unAccountedPresenceint (Sek.)Anwesenheit außerhalb aller Tagesblöcke und ohne Genehmigung
isCalculatedboolBerechnung erfolgreich (kein Fehler)
isApprovedboolManuell oder automatisch genehmigt
isRebookedboolBuchungen wurden manuell korrigiert
isHolidayboolTag ist ein Feiertag im Bundesland des Mitarbeiters
statusstring'valid' / 'invalid'
errorDescriptionstring'wrongBookings' oder 'noDayLines'
balanceCorrectionsOneToManyManuelle Saldokorrekturen
timeProfileWorkerJournalEvaluationsOneToManyAggregation 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).

FeldTypBedeutung
balancefloatKorrektur-Sekunden (positiv/negativ)
datedateWirksamkeitsdatum
reasonstringBegründungstext
isLockedboolSperre gegen weitere Bearbeitung
salaryTypeManyToOneOptionaler 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.

FeldTypBedeutung
idbigintPK
bookingTypeManyToOne TimeControlBookingTypeKlassifiziert die Buchung (Check-In/Out/Pause). Steuert die Reihenfolgevalidierung und das Erzeugen von BookingEntrys
workerManyToOne WorkerStempler
entryDateTimedatetimeZeitpunkt der Buchung
statusstringSTATUS_INVALID = 'invalid' oder STATUS_APPROVED = 'approved' (gesetzt durch TimeProfileBookingErrorProcessor)
descriptionstringFreitext (z. B. Kommentar im Buchungs-Dialog)
locationManyToOne Location (optional)Standort – Pflicht für bookingType.isOnSite=true
gpsLat, gpsLonstringGeokoordinaten (vom Browser via navigator.geolocation gesetzt)
isNewboolMarkierung für noch nicht synchronisierte Buchungen (Mobile/Offline)
isSyncedboolSynchronisationsstatus
isReRegistrationbooltrue, wenn aus einem genehmigten TimeControlBookingRequest erzeugt

Konstanten:

  • STATUS_INVALID = 'invalid' – mind. eine Validierung schlug fehl
  • STATUS_APPROVED = 'approved' – validiert und akzeptiert

5.2 Entity TimeControlBookingType – src/Entity/TimeControlBookingType.php​

Tabelle: timecontrolbookingtypes.

FeldTypWirkung
idbigintPK
namestringAnzeige (z. B. „Kommen", „Gehen", „Pause")
shortCodestringKürzel (z. B. „CI", „CO", „HO" für Home-Office)
descriptionstringFreitext
iconstringUI-Icon-Schlüssel
isCheckInboolValidierungs-Flag: Eine Buchung mit diesem Typ startet eine Anwesenheitsperiode. Nutzbar in TimeProfileBookingErrorProcessor::validateBookingOrder()
isCheckOutboolValidierungs-Flag: Beendet eine Anwesenheitsperiode. Pro System darf nur ein Typ isCheckOut=true aktiv sein (Toggle-Endpoint sorgt für Exklusivität)
isOnSiteboolErzwingt im FE-Buchungs-Dialog die Eingabe einer location. Bei Stempelung im Terminal-Modus wird ggf. die GPS-Position aus der Location ergänzt
isAvailableInTerminalModeboolSichtbar 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.

FeldWirkung
worker, entryDateTime, bookingType, location, descriptionwie TimeControlBooking
statusWorkflow-Status: STATUS_CREATED, STATUS_PENDING, STATUS_PRETESTED, STATUS_APPROVED, STATUS_DENIED
reasonBegrü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:

MethodeZweck
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​

MethodePfadZweck
GET/api/timecontrolbookingsListing (paginiert) – Query: worker, date, type
POST/api/timecontrolbookingsAnlegen (Hauptpfad). Triggert handleWorkBookingStatus() und ggf. checkTimeControlWorkStatus(), um offene Aufträge automatisch zu schließen
POST/api/timeControlBookingForceAnlegen ohne Duplikat-/Matching-Prüfung (Admin-Override)
PUT/api/timecontrolbookings/{id}Aktualisieren
DELETE/api/timecontrolbookings/{id}Löschen
GET/api/getcurrentbookingstatusLetzte Buchung des aktuellen Users (oder simulierten Check-Out)
GET/api/getRawBookingTimeVerstrichene Zeit seit letztem Check-In (Industrie-/Stunden-Format)
GET/api/bookingstatusbyworkerLetzte Buchung pro aktivem Mitarbeiter (Dashboard)
GET/api/getTimeControlStatusAggregat: checkInOnSite, checkInHomeOffice, checkInOnTrack, pause, checkedOut + aktive Abwesenheiten
GET/api/dashboard/numbersKennzahlen: present/absent/on_break/on_vacation
GET/api/bookingstatusbyterminalidLookup über Terminal-Seriennummer

Im createOrUpdate(Request) (privat) gilt:

  • Bei neuer Buchung wird der worker aus dem aktuellen User abgeleitet, wenn nicht explizit gesetzt.
  • entryDateTime defaultet auf „jetzt", wenn fehlend.
  • Duplikat-Schutz: Identische Buchung (gleicher Worker + gleicher Typ kurz vorher) wird nicht doppelt angelegt – die existierende wird zurückgegeben.
  • Wenn location gesetzt ist und (Terminal-Modus oder fehlende GPS-Koordinaten), werden GPS-Werte aus der Location ergänzt.

TimeControlBookingTypeController – src/Controller/TimeControlBookingTypeController.php​

MethodePfadZweck
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/getmappedbookingtypesKategorisierte 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​

KlasseZweck
TimeProfileBookingErrorProcessorValidierung der Buchungs-Reihenfolge und der Tagesblock-Zugehörigkeit; setzt status pro Buchung
TimeControlBookingRequestConversionProcessorGenehmigte Request → echter TimeControlBooking
TimeBookingDashboardAggregation pro Worker für Dashboards (currentBookingByWorker, currentBookingByWorkerWithoutImage)
TimeControlBookingReportProcessorPDF-Reporting
TimeControlBookingRequestListenerDoctrine 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​

PropertyTyp
idstring
booking_typeTimeControlBookingType
entry_date_timestring (ISO 8601)
is_new, is_syncedboolean
gps_lat, gps_lonstring
workerWorker
statusstring
descriptionstring
locationLocation | null
duration?number (FE-only, virtuell, in Millisekunden)

TimeControlBookingType – bookingtype.model.ts:

PropertyTyp
id, short_code, name, description, iconstring
isCheckIn, isCheckOut, isOnSite, isAvailableInTerminalModeboolean

5.9 Frontend-Services​

TimeControlService – timecontrol.service.ts​

MethodeEndpunktZweck
getBookingTypes()GET /api/getmappedbookingtypesKategorisiert nach checkInTypes (Terminal-gefiltert), checkOutType, otherTypes
getBookingStatus()GET /api/getcurrentbookingstatusAktueller Status; bei Fehlen → simulierter Check-Out
getRawBookingTime()GET /api/getRawBookingTimeVerstrichene Zeit, RxJS-Stream
getLocationService()–Browser-Geolocation-API
createNewBooking(body)POST /api/timecontrolbookingsBuchung anlegen

Subjects: onBookingTypesChanged, onBookingStatusChanged.

TimeControlBookingTypeService – bookingtypes.service.ts​

MethodeEndpunkt
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 Tages
  • POST /api/timecontrolbookings?admin=true – Manuelle Anlage durch HR/Admin
  • PUT /api/timecontrolbookings/{id}?admin=true – Manuelle Korrektur
  • DELETE /api/timecontrolbookings/{id} – Löschung

5.10 Frontend-Komponenten zu Stempelbuchungen​

KomponenteDateiZweck
TimeControlComponenttimecontrol.component.tsStempel-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)
TimeProfileWorkerBookingDialogV2Componenttime-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)
TimeProfileWorkerBookingItemV2Componenttime-profile-worker-v2/time-profile-worker-details-v2/time-profile-worker-booking-item-v2/Eine Buchungszeile im Journal mit Edit/Delete (sichtbar via actionsAllowed())
BookingtypesComponenttime-control-settings/bookingtypes/Settings-Liste; Spalten: shortcode, name, description, icon, checkin, checkout, isOnSite, isAvailableInTerminalMode, edit
TimecontrolbookingtypeDialogComponenttime-control-settings/bookingtypes/bookingtype-dialogs/Create/Edit Buchungstyp

5.11 End-to-End-Flow am Stempel-Terminal​

  1. TimeControlComponent.ngOnInit() ruft getBookingTypes() → GET /api/getmappedbookingtypes → Kategorisiert in checkInTypes (gefiltert nach isAvailableInTerminalMode), checkOutType, otherTypes.
  2. getCurrentBookingType() → GET /api/getcurrentbookingstatus → letzte Buchung oder simulierter Check-Out → Timer wird gestartet.
  3. Nutzer klickt einen Buchungs-Typ. Falls isOnSite=true und location noch nicht gewählt: Standortauswahl erzwingen.
  4. Browser-Geolocation abfragen (navigator.geolocation.getCurrentPosition), gps_lat/gps_lon setzen.
  5. POST /api/timecontrolbookings mit Payload:
    {
    "booking_type": { "id": "<typeId>" },
    "is_new": true,
    "is_synced": false,
    "gps_lat": "<lat>",
    "gps_lon": "<lon>",
    "location": { "id": "<locationId>" }
    }
  6. Backend: TimeControlBookingController::createOrUpdate() → Duplikat-Prüfung, Worker-/Datums-Defaults, ggf. GPS aus Location, Persist.
  7. handleWorkBookingStatus() schließt offene Auftragszeiten/Pausen, falls Anwesenheitsstatus wechselt.
  8. FE ruft erneut getCurrentBookingType(), aktualisiert Anzeige, entsperrt das UI (isLocked=false).

6. Backend-Komponenten​

6.1 Processoren – src/Processor/TimeProfiles/​

KlasseZweck
TimeProfileWorkerJournalCalculationProcessor.phpHauptberechnung pro Journal
ProfileTimeCalculationProcessor.phpErrechnet profileTime (Soll) eines TimeProfileDay als Summe aller Day-Lines mit calcStandardTime=true
TimeProfileWorkerJournalBalanceCalculationProcessor.phpAkkumulierten Saldo neu rechnen (aktuelles Journal + alle nachfolgenden genehmigten Journale + BalanceCorrections)
TimeProfileBookingErrorProcessor.phpMarkiert Buchungen als gültig/ungültig (validateBookingOrder, validateBookingPeriod)
TimeProfileWorkNotificationProcessor.phpBenachrichtigung bei unerfasster Präsenz
TimeWorkerJournalsCreationProcessor.phpBatch-Anlage von leeren Journalen
TimeWorkerJournalsDeletionProcessor.phpBatch-Bereinigung
BookingEntry.phpDTO eines Buchungs-Intervalls (Start, Ende, isPresent, isBreakTime, verbleibende Zeit)
TimeProfileCalendarEntryGenerationProcessor.phpAuto-Generierung von TimeProfileCalendar-Einträgen
TimeProfileWorkerJournalEvaluationProcessor.phpAggregation pro Standort/Buchungstyp
TimeProfileWorkerJournalReportProcessor.phpWochen-/Monatsaggregation für Reports

6.2 Wochenreport-Aggregation – src/TimeProfileWorkerJournalWeekReport/​

KlasseZweck
TimeProfileWorkerJournalWeekReportProcessorErzeugt aggregiertes Wochen-Reporting
WeekDataDTO Woche
WorkerDataDTO 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:

  • timeprofile
  • timeprofiletype
  • timeprofilegroup (Property allowed_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/​

KomponenteZweck
TimeProfileWorkerV2ComponentContainer mit Sidebar
TimeProfileWorkerListV2ComponentListenansicht aller Journale (paginiert, gefiltert) – Resolver resolveJournals
TimeProfileWorkerDetailsV2ComponentDetailansicht eines Journals: Arbeitszeit, Pause, Saldo, Differenz, Profilzeit; Aktionen Genehmigen, Berechnen, Unerfasste Präsenz akzeptieren/ablehnen, Buchung hinzufügen
TimeProfileWorkerFilterSidebarV2ComponentFilter (Datum, Geschäftsbereich, Abteilung, Team, Mitarbeiter, Status)
TimeProfileWorkerBookingItemV2ComponentEinzelne TimeControlBooking-Zeile
TimeProfileWorkerAbsenceItemV2ComponentAbwesenheitszeile
TimeProfileWorkerHolidayItemV2ComponentFeiertags-Indikator
TimeProfileWorkerBookingOverviewV2Component„Übersicht"-Tab (Aggregat)
TimeProfileWorkerBookingDialogV2ComponentDialog zum Anlegen/Bearbeiten einer Buchung

Property-Map TimeProfileWorkerDetailsV2Component:

  • selectedJournal, totalWorkTime, totalBreakTime, totalDiffTime, profileTime, balance
  • viewType: '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/​

RouteKomponenteZweck
time-profilesTimeProfileComponent + Detail/Tabs Mo–So + TimeProfileDayLineDialogComponentArbeitszeitmodelle pflegen
time-profile-typesTimeProfileTypeComponent + 3-Schritt-Stepper-DialogBerechnungs-Klassen pflegen
time-profile-groupsTimeProfileGroupComponent + Kalender-Subview + Worker-ZuordnungenSchichtplangruppen
booking-typesBookingtypesComponentStempel-Buchungstypen
time-chipsTimechipsComponentPhysische Karten
worker-chipsWorkerchipsComponentKarten ↔ Mitarbeiter
basic-settingsTimeControlBasicSettingsComponentGlobale Schalter (Tabelle Name/Modus)
journalsTimeProfileJournalModule (Settings-Variante)Journal-Konfiguration

7.4 Services​

ServiceVerantwortung
TimeProfileJournalService (time-profile-worker/time-profile-journal.service.ts)V1-Listing/Filter, calculate(), update(), getClosedMonths()
TimeProfileWorkerV2Service, TimeProfileWorkerDetailsV2Service, TimeProfileWorkerFilterV2ServiceV2-Listing, Detail, Filter; ruft u. a. GET /api/v2/timeProfileWorkerJournals/events/{id} für Buchungen
TimeProfileServiceCRUD TimeProfile, BreakSettings
TimeProfileGroupServiceCRUD TimeProfileGroup, Worker-Zuordnung, startCalendarEntryGeneration()
TimeProfileCalendarServiceCRUD TimeProfileCalendar
TimeProfileTypeServiceCRUD TimeProfileType
BookingtypesService, TimechipsService, WorkerchipsServiceCRUD Settings
TimecontrolServiceStempel-UI: Buchung anlegen, Geolocation
TimeControlBasicSettingsServiceis_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-ControlTypLabel DEWirkung in der Berechnung
nametextNameAnzeige
descriptiontextBeschreibungAnzeige
colorFarbpickerFarbeUI-Darstellung in Liste/Kalender
profile_timenumberProfilzeitOptional manuell. Wird normalerweise aus Day Lines berechnet (siehe ProfileTimeCalculationProcessor::calculateProfileTime).
is_fallbackToggleIst FallbackSteuert die Profilauswahl bei Tagesanlage: Mitarbeiter ohne expliziten TimeProfileCalendar-Eintrag erhalten dieses Modell.
count_break_partiallyTogglePause teilweise zählenVerä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_periodSelect (required)BerechnungBestimmt 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_agreementSucheLohnvereinbarungVerknüpfung für Lohnabrechnungen

8.2 TimeProfileBreakSetting-Dialog – Pflichtpausen​

Form-ControlTypLabel DEWirkung
time_limitSekundenSchwelleWenn (geleistete Arbeitszeit) ≥ Schwelle, wird break_length verpflichtend
break_lengthSekundenPausenlängeSoll-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-ControlTypLabel DEWirkung
start_timeZeit (HH:mm)StartzeitBlock-Beginn; relevant für Überlappungsprüfung mit Buchungen
end_timeZeit (HH:mm)EndzeitBlock-Ende
tolerance_before_startint (Min.)Toleranz vor StartStempelzeit vor dem Blockbeginn wird ignoriert (subtrahiert), wenn sie innerhalb dieser Toleranz liegt – siehe isWithinTolerance()
tolerance_after_startint (Min.)Toleranz nach Start(Schalter im Modell vorhanden; aktuell mit // FIXME: Check for tolerance after start and before end annotiert)
tolerance_before_endint (Min.)Toleranz vor Ende(siehe tolerance_after_start)
tolerance_after_endint (Min.)Toleranz nach EndeStempelzeit nach dem Blockende wird ignoriert (subtrahiert), wenn sie innerhalb dieser Toleranz liegt
time_profile_typeSelectProfiltypBestimmt die Bewertung des Blocks (siehe § 8.4)

Bei time_profile.calculation_period === '12_12' werden Day-Lines mit start_time ≥ 12:00 im FE als datierte Werte (Date-String inkl. „heute"-Datum) gehalten und über new Date(...).getTime() sortiert; Day-Lines mit start_time < 12:00 bleiben als reiner HH:mm-String erhalten. Im Backend werden diese Zeiten in getAdjustedDate() 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-ControlTypLabel DEWirkung
nameText (max 60)NameAnzeige
codeSelectCodeKlassifikator – 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-ControlLabel DEWirkung in TimeProfileWorkerJournalCalculationProcessor::calculatePaidTime()
calc_standard_timeStandardzeitBlock 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_timeEntlohnte ZeitBlock wird ebenfalls in der Berechnung berücksichtigt (zusätzlicher Pass-Filter neben calc_standard_time), zählt aber nicht auf die Profilzeit.
calc_paid_overtimeEntlohnte ÜberstundenzeitBlock fließt in paid_overtime ein (Über-Soll).
calc_justified_absenceBerechtigte AbwesenheitBlock 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_absenceUnberechtigte AbwesenheitBlock fließt in unpaid_absence_time.
calc_flexible_working_hours_plusGleitzeit+Block trägt zu paid_overtime bei (zusammen mit calc_paid_overtime).
calc_flexible_working_hours_minusGleitzeit−Block wird als negative Gleitzeit erfasst (Saldo-Reduktion).

Wichtig: Mehrere Flags können auf demselben Typ aktiv sein – z. B. calc_rewarded_time und calc_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-ControlLabel DEWirkung
rewardedTimeEntlohnte ZeitLohnrelevante Zeit
paidOvertimeEntlohnte ÜberstundenzeitLohnrelevante Überstunden
breakPausePause wird vergütet
flexibleWorkingHoursGleitzeit−Negative Gleitzeit lohnrelevant
flexibleWorkingHoursPlusGleitzeit+Positive Gleitzeit lohnrelevant
allowancesZulagenZuschläge berücksichtigen
absenceAbwesenheitAbwesenheiten lohnrelevant

8.5 TimeProfileGroup-Dialog – Schichtplan​

Form-ControlTypLabel DEWirkung
name, descriptiontextName / BeschreibungAnzeige
time_profileSelectZeitprofilModell, das Mitgliedern zugewiesen wird
auto_generate_calendar_entriesToggleKalendereinträge automatisch generierenWenn aktiv, erzeugt TimeProfileCalendarEntryGenerationProcessor automatisch tagesgenaue TimeProfileCalendar-Datensätze (gem. allowed_week_days)
allowed_week_daysMulti-SelectErlaubte WochentageSchränkt die Auto-Generierung auf bestimmte Wochentage ein

8.6 TimeProfileCalendar-Dialog – Kalendereintrag​

Form-ControlTypLabel DEWirkung
datedateDatumKonkreter Tag
workerSelectMitarbeiterEmpfänger
time_profileSelectZeitprofilTagsgenauer Override (höchste Priorität bei der Profilauswahl)
time_profile_groupSelect (optional)GruppeZur Nachverfolgung der Quelle

Hilfskomponente: CopyTimeProfileCalendarDaysComponent zum Kopieren von Kalendereinträgen über Zeiträume.

8.7 Buchungs-Dialog (V2) – TimeProfileWorkerBookingDialogV2​

Form-ControlTypLabel DEWirkung
bookingTypeAutocomplete (required)BuchungstypWählt TimeControlBookingType (Check-In/Check-Out/Pause/…) – steuert Validierung in TimeProfileBookingErrorProcessor
locationSelect (conditional)OrtPflicht, wenn bookingType.is_on_site === true
startTimeZeit (HH:mm, required)ZeitBuchungszeitstempel
commentText (required)KommentarBegrü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-ControlTypLabel DEWirkung
nametextNameAnzeige
shortcodetextKürzelBerichts-Spalte
descriptiontextBeschreibungAnzeige
iconSelectIconUI-Symbol
isOnSiteToggleVor-OrtErzwingt location-Pflicht im Buchungs-Dialog
inTerminalModeToggleAnzeige TerminalmodusSchaltet Buchungstyp im Terminal-Stempel-Modus sichtbar
is_check_in / is_check_outToggle (Switch-Endpoint)Check-In/OutSteuert 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_invalid
  • startDate, endDate, month, year
  • businessAreas[], departments[], locations[], teams[]
  • onlyMy (Toggle „Eigene Journale")

8.12 Detailansicht – Aktionen​

Im TimeProfileWorkerDetailsV2Component (View-Toggle bookings/overview):

ButtonSichtbar wennAktion
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 > 0declineUnAccountedPresence()
Unerfasste Präsenz genehmigen (check_circle)actionsAllowed() && totalDiffTime > 0approveUnAccountedPresence() – 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​

  1. Lese aus dem Journal: date, worker, timeProfile.
  2. Wochentag (monday … sunday) bestimmen → TimeProfileDay und alle TimeProfileDayLines laden, sortiert nach startTime.
  3. Tagesfenster ermitteln und alle TimeControlBookings über TimeControlBookingRepository::findInRangeFiltered($start, $end, $worker, null, true) lesen. Das Fenster hängt von timeProfile.calculationPeriod ab:
    • '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.
  4. 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 mit STATUS_INVALID.
    • Periode (validateBookingPeriod): Jede Buchung muss in eine TimeProfileDayLine fallen (sonst invalid, sofern nicht unAccountedPresenceCleared vom FE gesetzt ist).
    • Hat eine Buchung Fehler, wird journal.status = 'invalid' und errorDescription = 'wrongBookings' (oder 'noDayLines', wenn das Profil für den Wochentag keine Day-Lines hat).
  5. BookingEntry-DTOs aus aufeinanderfolgenden Buchungspaaren erstellen, jeweils mit:
    • startDate, endDate
    • isPresent: true, wenn das erste Pair-Element ein Check-In ist
    • isBreakTime: 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 von nextDayLine.toleranceBeforeStart Minuten 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 mit previousDayLine.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 mit calcJustifiedAbsence = true liegen
  • totalBreakNotPlanned: Pausen-BookingEntrys in Blöcken ohne dieses Flag
  • totalPlannedWithinPresence: 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 volle breakLength verlangt. 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 += sumOfUnAccountedForPresence
  • sumOfUnAccountedForPresence = 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)​

  1. Primäre Arbeitsadresse des Mitarbeiters → Bundesland (State).
  2. PublicHoliday für journal.date und PublicHolidayState für dieses Bundesland prüfen.
  3. Aktuelle Employment für den Mitarbeiter laden:
    • Existiert employment.isHolidayWorkAllowed? Dann isAllowed = true.
    • Andernfalls: isHolidayWork = true, isAllowed = false.
  4. Wenn Feiertag und nicht erlaubt: paid_absence_time wird auf profile_time gesetzt (kompletter Tag wird als bezahlte Abwesenheit verbucht).
  5. journal.is_holiday = true wird gesetzt.

9.9 Tagessaldo​

balance = paidWorkingTime + paidAbsenceTime + paidOvertime − profileTime

Hinweis: Da paidWorkingTime + paidOvertime bereits durch den Cap (§ 9.5) so verteilt sind, dass paidWorkingTime ≤ profileTime, repräsentiert balance faktisch den Anteil über/unter Soll.

9.10 Kumulativer Saldo (balance_accumulated)​

Implementiert in TimeProfileWorkerJournalBalanceCalculationProcessor::calculateBalances():

  1. Suche letztes genehmigtes Journal vor journal.date desselben Mitarbeiters → previousJournal.balance_accumulated als Startwert.
  2. Sammle dieses Journal + alle nachfolgenden genehmigten Journale (findNextApprovedJournals).
  3. Pro Journal:
    • Lade alle BalanceCorrection-Datensätze für (worker, date) und summiere → balanceCorrection.
    • accumulatedBalance += balanceCorrection + journal.balance
    • Persistiere journal.balance_accumulated = accumulatedBalance.
  4. 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:

WertTagesfensterAnwendungsfall
'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 TagesSchicht-/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 (isWithinTolerance mit nextDayLine.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​

StelleDateiVerhalten
TimeProfile-ModellTimeProfile.model.tsNeue 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-Formtime-profile-dialog.component.ts und .component.htmlNeuer 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 KonstruktorTimeProfileDayLine.model.tsBei 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-Sortierungtime-profile-day.datasource.ts#L46Bei '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-Headertime-profile-worker-details-v2.component.ts#L42 und .html#L155Properties 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 V2time-profile-worker-booking-dialog-v2.component.ts#L83Beim 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-Typtime-profile-worker-types.tsTimeProfile-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 in isWithinTolerance() operiert auf den per getAdjustedDate() projizierten Zeiten und benötigt deshalb keine separate Sonderbehandlung.
  • Saldo: balance und balance_accumulated sind 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 mit journal.date als 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 = !hasErrors
  • is_approved = !hasErrors
  • un_accounted_presence = sumOfUnAccountedForPresence
  • start_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_approved per PUT /api/timeprofileworkerjournals/{id}
  • „Unerfasste Präsenz" – Aktion approve bzw. decline (siehe § 9.6); der Backend-Parameter heißt unAccountPresenceAction

11. API-Endpunkte​

11.1 Arbeitszeitsjournale (V1) – TimeProfileWorkerJournalController.php​

MethodePfadZweck
GET/api/timeprofileworkerjournalsListing (paginiert, gefiltert)
GET/api/find/timeprofileworkerjournalsSuche
GET/api/timeprofileworkerjournals/{id}Einzelnes Journal
POST/api/timeprofileworkerjournalsAnlegen
PUT/api/timeprofileworkerjournals/{id}Aktualisieren (z. B. is_approved)
DELETE/api/timeprofileworkerjournals/{id}Löschen
GET/api/timeprofileworkerjournals/{id}/calculateManuelle Neuberechnung
GET/api/getlatesttimeprofileworkerjournals/{workerId}Letzte Journale eines Mitarbeiters
GET/api/mytimeprofilejournalsEigene Journale
GET/api/latestjournalsforcurrentuserLetzte eigene
GET/api/timeprofileworkerjournalsWithCorrectionsInkl. BalanceCorrections
GET/api/timeProfileWorkerJournalsDataForDayTagesdaten
GET/api/timeProfileWorkerJournalsDataForWeekWochendaten

11.2 Arbeitszeitsjournale (V2) – TimeProfileWorkerJournalControllerV2.php​

MethodePfad
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:

MethodePfadZweck
GET/POST/PUT/DELETE/api/timecontrolbookings[/{id}]CRUD Stempelungen (mit ?admin=true für manuelle Anlage durch HR/Admin)
POST/api/timeControlBookingForceAnlegen ohne Duplikat-/Matching-Prüfung
GET/api/getcurrentbookingstatusAktueller Status des Users
GET/api/getRawBookingTimeVerstrichene Zeit
GET/api/getmappedbookingtypesKategorisierte 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/timeprofiles
  • TimeProfileDayController, TimeProfileDayLineController
  • TimeProfileTypeController
  • TimeProfileBreakSettingController
  • TimeProfileGroupController, TimeProfileGroupWorkerController
  • TimeProfileCalendarController
  • TimeProfileWorkerJournalReportController – Berichte

12. Berechtigungen​

Die Berechtigungsprüfung erfolgt dreistufig:

  1. Route-Guards (canActivate) verhindern die Navigation zu nicht erlaubten Routen.
  2. Komponenten-Methoden (z. B. actionsAllowed()) blenden UI-Aktionen aus, wenn die Permission fehlt oder die Logik (Eigentümer-Check) nicht erfüllt ist.
  3. 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/work benötigt projects_READ UND timecontrol_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 per POST /auth/me und 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​

AspektDetails
BackendPermissionEnum::TimeProfileJournalAccess, beschrieben mit „Access to the time profile worker journals"
FE-VerwendungRoute-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 HinweisLese-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​

AspektDetails
BackendPermissionEnum::TimeProfileJournalEditAll – „Allows user to edit all time profile journals"
FE-VerwendungIn 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​

AspektDetails
BackendPermissionEnum::TimeProfileJournalEditOthers – „Allows user to edit all time profile journals except for their own"
FE-VerwendungKombination im selben actionsAllowed()
LogikuserHasPermission(user, 'time_profile_journal_edit_all') ‖ (userHasPermission(user, 'time_profile_journal_edit_others') ∧ journal.workerid !== currentWorker.workerid)
Schaltet freiGleiche 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-Aufrufewie edit_all, server-seitig spiegelbildlich gegen den Worker-Owner geprüft

12.5 Permission-Detail: timecontrol_user​

AspektDetails
BackendPermissionEnum::TimeControlUser – „can access time control module"
FE-VerwendungRoute-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-AufrufeAlle /api/timecontrolbookings* POST/PUT/GET, /api/getcurrentbookingstatus, /api/getRawBookingTime, /api/getmappedbookingtypes, sämtliche Auftragszeit-Endpunkte

12.6 Permission-Detail: projects_READ​

AspektDetails
BackendPermissionEnum::ProjectsRead – „display a project"
FE-VerwendungRoute-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​

AspektDetails
BackendPermissionEnum::TimeControlAdmin = 'timecontrol_settings' – „Has access to the settings time control module"
FE-VerwendungRoute-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 freiKonfiguration aller Stamm- und Konfigurationsdaten des Features (Profile, Profiltypen, Gruppen, Pausen, Kalender, Buchungstypen, Karten)

12.8 Permission-Detail: timecontrol_journals​

AspektDetails
BackendPermissionEnum::TimeControlJournals – „Has access to the journals time control module"
FE-VerwendungRoute-Guard auf /apps/timecontrol/settings/journals (lazy TimeProfileJournalModule)
Schaltet freiErweiterte Journal-Konfiguration (über die Standard-Settings hinaus)

12.9 Permission-Detail: timecontrol_shift_planner​

AspektDetails
BackendPermissionEnum::TimeControlShiftPlanner – „can access shift planner"
FE-VerwendungRoute-Guard auf /apps/holidayPlanner/shift-planner (calendar.module.ts)
Schaltet freiSchichtplaner-Werkzeug innerhalb des Holiday-Planner-Moduls

12.10 Permission-Detail: timecontrol_re_registration_other & timecontrol_re_registration_date​

AspektDetails
BackendPermissionEnum::TimecontrolReRegistrationOther und …RegistrationDate
FE-VerwendungEigene 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(...).

PermissionRouteKomponente
superset_time_journal_report_read/apps/time-journal-reportTimeJournalDashboardComponent
superset_working_time_report/apps/working-time-reportSupersetWorkingTimeReportComponent
superset_salary_dashboard_read/apps/salary-dashboardSalaryDashboardComponent
superset_time_control_booking_location_report/apps/hrm/booking-location-reportBookingLocationReportComponent

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​

AspektDetails
BackendPermissionEnum::CreateSalaryElements – „Permission to create new salary elements"
FE-VerwendungIn den Salary-Verwaltungskomponenten (Dialog-Open-Logik, „Neu"-Buttons)
Schaltet freiAnlage 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:

PermissionWirkung in Journal-Kontext
approve_TIMEPROFILEJOURNALS, supervisor_hr_clerk_journals, hr_administrator_journalsGenehmigungs-Workflow durch verschiedene Rollen
workers_READVoraussetzung, um Worker-Daten in Filtern und Listen zu laden
all_ABSENCESSichtbarkeit von Abwesenheits-Einträgen im Journal-Detail
holiday_planner_time_profile_change_others / _allTagesgenauer Wechsel des Zeitprofils anderer/aller Mitarbeiter aus dem Holiday-Planner
presence_dashboard_my_all_filter, presence_dashboard_absence_registrationAnwesenheits-Dashboard-Filter und Abwesenheits-Anlage
balance_dashboard_accessSaldo-Dashboard-Zugriff
absence_creation_vacation_lock_bypass, vacation_edit_detailsSonderfälle bei Urlaubssperre

12.14 Permission-Matrix (Feature → Permission → Route → API)​

FeaturePermission(s)FE-EintrittspunktSichtbarkeits-/AktivierungslogikBackend-Endpoints
Journale ansehentime_profile_journal_access/timecontrol/time-profile-worker-journalsrequirePermission Route-GuardGET /api/v2/timeProfileWorkerJournals, …/find, …/{id}/thumbnail, …/events/{id}, GET /api/timeprofileworkerjournals/{id}
Journal genehmigen / berechnentime_profile_journal_edit_all ‖ (time_profile_journal_edit_others ∧ Fremd-Journal)Selbe Route + actionsAllowed()Methodischer Check + *ngIfPUT /api/timeprofileworkerjournals/{id}, GET /api/timeprofileworkerjournals/{id}/calculate
Stempelung im Journal hinzufügen / ändern / löschengleich wie obenButtons im Detail/ItemactionsAllowed()POST /api/timecontrolbookings?admin=true, PUT /…?admin=true, DELETE /api/timecontrolbookings/{id}
Eigene Stempelung am Terminaltimecontrol_user/timecontrol/mainRoute-GuardGET /api/getmappedbookingtypes, GET /api/getcurrentbookingstatus, GET /api/getRawBookingTime, POST /api/timecontrolbookings
Auftragszeitbuchungtimecontrol_user ∧ projects_READ/timecontrol/workBeide Route-Guards (AND)/api/timecontrolworkbookings*, Project-Filter-Endpoints
Rechnungen aus Zeitbuchungentimecontrol_user/timecontrol/invoiceRoute-GuardInvoice-Endpoints
Stammdaten konfigurierentimecontrol_settings/timecontrol/settings/*Route-Guard pro Sub-RouteCRUD /api/timeprofiles, /api/timeprofiletypes, /api/timeprofilegroups, /api/timecontrolbookingtypes, /api/timechips, /api/workerchips
Journal-Konfigurationtimecontrol_journals/timecontrol/settings/journalsRoute-GuardJournal-Settings-Endpoints
Schichtplanertimecontrol_shift_planner/holidayPlanner/shift-plannerRoute-GuardShift-Planner-Endpoints
Nach-Stempel-Antrag (für andere/datum)timecontrol_re_registration_other / …_dateRe-Registration-KomponentenEigene Guards/api/timeControlBookingRequest*
BI Time-Journalsuperset_time_journal_report_read/time-journal-reporthasSupersetConfig ∧ requirePermissionSuperset Embed
BI Working Timesuperset_working_time_report/working-time-reportditoSuperset Embed
BI Salarysuperset_salary_dashboard_read/salary-dashboardditoSuperset Embed
BI Booking Location (HRM)superset_time_control_booking_location_report/hrm/booking-location-reportditoSuperset Embed
Lohnarten anlegencreate_salary_elementsSalary-ModulKomponenten-CheckSalary-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 von TimeControlBookings aus dem Journal-Detail ist nur ein FE-Hinweis – die tatsächliche Autorisierung passiert serverseitig (Voter / #[IsGranted] im TimeControlBookingController).
  • Für time_profile_journal_edit_others findet 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äftsbereich
  • time-control-booking-report.component – Detail-Buchungsreport (PDF via TimeControlBookingReportProcessor)

Dashboards​

  • time-journal-dashboard.component
  • superset-balance-dashboard.component – BI-Dashboard Saldo (Apache Superset Embed)
  • superset-working-time-report.component – BI-Dashboard Arbeitszeit
  • salary-dashboard.component
  • booking-location-report.component (HRM-Modul)

Backend​

  • TimeProfileWorkerJournalReportController
  • TimeControlBookingReportController – GET /api/reports/timecontrolbooking
  • TimeProfileWorkerJournalReportProcessor
  • TimeProfileWorkerJournalWeekReportProcessor, WeekData, WorkerData
  • TimeBookingDashboard – Aggregations-Processor für Anwesenheits-Dashboards (getTimeControlStatus, dashboard/numbers)

14. Console Commands & geplante Jobs​

Alle unter bin/console:

CommandKlasseZweck
app:create-time-profile-work-journalsCreateTimeProfileWorkJournalsCommandTägliche Anlage leerer Journale für aktive Mitarbeiter
app:calculate-time-profile-journalsCalculateTimeProfileJournalsCommandBatch-Berechnung offener Journale
app:calculate-time-profile-timesCalculateTimeProfileTimesCommandProfilzeit-Vorberechnung
app:time-profile-calendar-entry-generationTimeProfileCalendarEntryGenerationCommandAuto-Generierung von TimeProfileCalendar-Einträgen pro Gruppe
app:generate-time-profile-worker-journal-evaluationGenerateTimeProfileWorkerJournalEvaluationCommandEvaluation-Datensätze für Reports/Lohn
app:delete-duplicate-time-profile-journalsDeleteDuplicateTimeProfileJournalsCommandBereinigung doppelter Journale
app:create-test-time-control-bookings <worker_id>CreateTestTimeControlBookingsTestdaten für Stempelungen (Mo–Fr, 8:00–17:00)
app:time-profile-group-worker-migrationTimeProfileGroupWorkerMigrationCommandDatenmigration Gruppen↔Mitarbeiter
app:time-profile-and-salary-syclient-migrationTimeProfileAndSalarySyClientMigrationCommandMulti-Tenant-Migration
app:create-test-time-profile-calendar-entriesCreateTestTimeProfileCalendarEntriesCommandTestdaten Kalender
app:time-control-bookings-migrateTimeControlBookingsMigrateCommandDatenmigration Stempelungen
app:migrate-time-profile-calculation-periodTimeProfileCalculationPeriodCommandMigriert 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>PollForNewTimeControlBookingsCommandPollt 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:

Flagpaid_working_timepaid_overtimeunpaid_absence_timepaid_absence_timePausen-TöpfeProfilzeit (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:

SchalterWirkung
time_profile.is_fallbackDefault-Profil bei fehlender Zuweisung
time_profile.count_break_partiallyPflichtpause 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_entriesAuto-Erzeugung tagesgenauer Calendar-Einträge
time_profile_group.allowed_week_daysAuf welche Wochentage auto_generate wirkt
time_control_booking_type.is_on_siteErzwingt location im Buchungs-Dialog
time_control_booking_type.is_available_in_terminal_modeSichtbar im Stempel-Terminal
time_control_booking_type.is_check_in / is_check_outSteuert Reihenfolgevalidierung

Anhang B – Kritische Stellen / Known Issues​

  • TimeProfileWorkerJournalCalculationProcessor::calculatePaidTime() enthält ein // FIXME: Check for tolerance after start and before end – aktuell werden tolerance_after_start und tolerance_before_end nicht ausgewertet, nur tolerance_before_start (über die nächste DayLine) und tolerance_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 BreakSettings enthält auskommentierte Variante (Block 109–111 in TimeProfileWorkerJournalCalculationProcessor); aktiv ist die Variante über totalPlannedWithinPresence.
  • TimeProfileType.calc_flexible_working_hours_minus ist im Datenmodell vorhanden, wird im Berechnungsprozessor jedoch derzeit nicht aktiv ausgewertet (außer als Klassifikator) – ggf. später ergänzen.
  • Statusübergang valid ↔ invalid triggert ausschließlich auf preUpdate von Doctrine; bei direkten DQL-Updates wird der Listener nicht aufgerufen.
  • Der ?admin=true-Query-Parameter beim Anlegen/Ändern von TimeControlBookings aus dem Journal-Detail ist nur eine FE-Konvention; die echte Autorisierung muss serverseitig erfolgen.
  • TimeProfile.calculation_period ist DB-seitig nullable (VARCHAR(255) DEFAULT NULL), das FE-Form-Feld ist jedoch required mit nur zwei zulässigen Werten. Bestandsdaten müssen mit app:migrate-time-profile-calculation-period auf '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-Fallback false, was beim Edit eines Alt-Profils ohne Period zu einem leeren Select-Wert führt.
  • ProfileTimeCalculationProcessor::calculateProfileTime() und getAdjustedDate() implementieren die '12_12'-Sonderlogik unabhängig voneinander. Beide verwenden den Vergleich ≥ 12:00, behandeln den Grenzfall „Block startet exakt 12:00" aber unterschiedlich (in calculateProfileTime zählt der Block ohne Tagesverschiebung, in getAdjustedDate wird er auf den Vortag projiziert). Bei der Konfiguration von Day-Lines genau auf der Mittagsgrenze ist Vorsicht geboten.

Ende der Dokumentation.