Repository: urlaubsverwaltung/urlaubsverwaltung (java)

Issue:
Abwesenheitsübersicht wirft einen Fehler, da die  "Farbe" nicht vorhanden ist
Siehe dazu

https://github.com/urlaubsverwaltung/urlaubsverwaltung/blob/9e7c537137e7afdc8b3577e446b6fdd7d3983e6a/src/main/java/org/synyx/urlaubsverwaltung/absence/web/AbsenceOverviewViewController.java#L509

Entweder kommt hier ein Feiertag eine Nicht-Arbeitstag oder eine Krankmeldung an, welche keinen Farbe hat.

@bseber sollten wir hier ggf einen Default ausspielen, statt eine exception zu werfen?

```
2023-12-20T13:53:10.077Z ERROR 1 --- [cat-handler-479] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: java.util.NoSuchElementException: No value present] with root cause

java.util.NoSuchElementException: No value present
	at java.base/java.util.Optional.orElseThrow(Unknown Source) ~[na:na]
	at org.synyx.urlaubsverwaltung.absence.web.AbsenceOverviewViewController.recordInfoToColor(AbsenceOverviewViewController.java:509) ~[classes/:na]
	at org.synyx.urlaubsverwaltung.absence.web.AbsenceOverviewViewController.lambda$absenceOverview$3(AbsenceOverviewViewController.java:157) ~[classes/:na]
	at org.synyx.urlaubsverwaltung.absence.web.AbsenceOverviewViewController.getAbsenceOverviewDayTypeForHalfDay(AbsenceOverviewViewController.java:381) ~[classes/:na]
	at org.synyx.urlaubsverwaltung.absence.web.AbsenceOverviewViewController.getAbsenceOverviewDayType(AbsenceOverviewViewController.java:325) ~[classes/:na]
	at org.synyx.urlaubsverwaltung.absence.web.AbsenceOverviewViewController.getAbsenceOverviewDayType(AbsenceOverviewViewController.java:302) ~[classes/:na]
	at org.synyx.urlaubsverwaltung.absence.web.AbsenceOverviewViewController.lambda$getAbsenceOverViewMonthModels$11(AbsenceOverviewViewController.java:249) ~[classes/:na]
	at java.base/java.util.Optional.map(Unknown Source) ~[na:na]
	at org.synyx.urlaubsverwaltung.absence.web.AbsenceOverviewViewController.getAbsenceOverViewMonthModels(AbsenceOverviewViewController.java:249) ~[classes/:na]
	at org.synyx.urlaubsverwaltung.absence.web.AbsenceOverviewViewController.absenceOverview(AbsenceOverviewViewController.java:160) ~[classes/:na]
	at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source) ~[na:na]
	at java.base/java.lang.reflect.Method.invoke(Unknown Source) ~[na:na]
	at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:254) ~[spring-web-6.1.1.jar:6.1.1]
	at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:182) ~[spring-web-6.1.1.jar:6.1.1]
	at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:118) ~[spring-webmvc-6.1.1.jar:6.1.1]
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:917) ~[spring-webmvc-6.1.1.jar:6.1.1]
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:829) ~[spring-webmvc-6.1.1.jar:6.1.1]
	at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-6.1.1.jar:6.1.1]
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1089) ~[spring-webmvc-6.1.1.jar:6.1.1]
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:979) ~[spring-webmvc-6.1.1.jar:6.1.1]
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014) ~[spring-webmvc-6.1.1.jar:6.1.1]
	at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:903) ~[spring-webmvc-6.1.1.jar:6.1.1]
```

Pull request description:
fix absence overview with a noon christmaseve absence
closes #4429
<!--

Thanks for contributing to the Urlaubsverwaltung.
Please review the following notes before submitting you pull request.

Please look for other issues or pull requests which already work on this topic. Is somebody already on it? Do you need to synchronize?

# Security Vulnerabilities

🛑 STOP! 🛑 If your contribution fixes a security vulnerability, please do not submit it.
Instead, please open a report of a security vulnerability via
[Report a security vulnerability](https://github.com/urlaubsverwaltung/urlaubsverwaltung/security/advisories/new)


# Describing Your Changes

If, having reviewed the notes above, you're ready to submit your pull request, please
provide a brief description of the proposed changes.

If they:
 🐞 fix a bug, please describe the broken behaviour and how the changes fix it.
    Please label with 'type: bug' and 'status: new'
    
 🎁 make an enhancement, please describe the new functionality and why you believe it's useful.
    Please label with 'type: enhancement' and 'status: new'
 
If your pull request relates to any existing issues,
please reference them by using the issue number prefixed with #.

-->


Patch:
diff --git a/src/main/java/org/synyx/urlaubsverwaltung/absence/web/AbsenceOverviewViewController.java b/src/main/java/org/synyx/urlaubsverwaltung/absence/web/AbsenceOverviewViewController.java
index 1f6056b16..efd90deb7 100644
--- a/src/main/java/org/synyx/urlaubsverwaltung/absence/web/AbsenceOverviewViewController.java
+++ b/src/main/java/org/synyx/urlaubsverwaltung/absence/web/AbsenceOverviewViewController.java
@@ -364,7 +364,7 @@ public class AbsenceOverviewViewController implements HasLaunchpad {
         boolean ignoreMorning = hasGenericAbsenceType(morning, AbsencePeriod.AbsenceType.NO_WORKDAY)
             || hasGenericAbsenceType(morning, AbsencePeriod.AbsenceType.PUBLIC_HOLIDAY);
         boolean ignoreNoon = hasGenericAbsenceType(noon, AbsencePeriod.AbsenceType.NO_WORKDAY)
-            || hasGenericAbsenceType(morning, AbsencePeriod.AbsenceType.PUBLIC_HOLIDAY);
+            || hasGenericAbsenceType(noon, AbsencePeriod.AbsenceType.PUBLIC_HOLIDAY);
 
         if (!ignoreMorning && morning.isPresent()) {
             if (anonymizeMorning) {
@@ -383,7 +383,7 @@ public class AbsenceOverviewViewController implements HasLaunchpad {
             }
         }
 
-        if (!ignoreMorning) {
+        if (!ignoreMorning && morning.isPresent()) {
             final boolean morningWaiting = morning.map(AbsencePeriod.RecordInfo::hasStatusWaiting).orElse(false);
             if (morningWaiting) {
                 return anonymizeMorning ? builder.absenceMorning() : builder.waitingAbsenceMorning();
@@ -405,7 +405,7 @@ public class AbsenceOverviewViewController implements HasLaunchpad {
             }
         }
 
-        if (!ignoreNoon) {
+        if (!ignoreNoon && noon.isPresent()) {
             final boolean noonWaiting = noon.map(AbsencePeriod.RecordInfo::hasStatusWaiting).orElse(false);
             if (noonWaiting) {
                 return anonymizeNoon ? builder.absenceNoon() : builder.waitingAbsenceNoon();


Test patch:
diff --git a/src/test/java/org/synyx/urlaubsverwaltung/absence/web/AbsenceOverviewViewControllerTest.java b/src/test/java/org/synyx/urlaubsverwaltung/absence/web/AbsenceOverviewViewControllerTest.java
index 7076376e3..8bcdf2c14 100644
--- a/src/test/java/org/synyx/urlaubsverwaltung/absence/web/AbsenceOverviewViewControllerTest.java
+++ b/src/test/java/org/synyx/urlaubsverwaltung/absence/web/AbsenceOverviewViewControllerTest.java
@@ -42,6 +42,7 @@ import java.util.Locale;
 import java.util.Map;
 import java.util.stream.Stream;
 
+import static java.time.Month.DECEMBER;
 import static java.time.Month.JANUARY;
 import static java.util.Collections.emptyList;
 import static org.hamcrest.CoreMatchers.allOf;
@@ -68,6 +69,8 @@ import static org.synyx.urlaubsverwaltung.application.vacationtype.VacationTypeC
 import static org.synyx.urlaubsverwaltung.application.vacationtype.VacationTypeColor.ORANGE;
 import static org.synyx.urlaubsverwaltung.application.vacationtype.VacationTypeColor.YELLOW;
 import static org.synyx.urlaubsverwaltung.period.DayLength.FULL;
+import static org.synyx.urlaubsverwaltung.period.DayLength.MORNING;
+import static org.synyx.urlaubsverwaltung.period.DayLength.NOON;
 import static org.synyx.urlaubsverwaltung.person.Role.INACTIVE;
 import static org.synyx.urlaubsverwaltung.person.Role.OFFICE;
 import static org.synyx.urlaubsverwaltung.person.Role.USER;
@@ -3818,6 +3821,132 @@ class AbsenceOverviewViewControllerTest {
     // ---
     // half VACATION, half SICK
 
+    @ParameterizedTest
+    @EnumSource(value = Role.class, names = {"BOSS", "OFFICE"})
+    void ensureChristmasEveOnMorningForPrivilegedPerson(Role role) throws Exception {
+
+        final LocalDate december24 = LocalDate.of(2024, DECEMBER, 24);
+        final LocalDate firstOfMonth = LocalDate.of(2024, DECEMBER, 1);
+        final LocalDate lastOfMonth = LocalDate.of(2024, DECEMBER, 31);
+        final DateRange dateRange = new DateRange(firstOfMonth, lastOfMonth);
+
+        final var person = new Person();
+        person.setId(1L);
+        person.setPermissions(List.of(USER, role));
+        person.setFirstName("Bruce");
+        person.setLastName("Springfield");
+        person.setEmail("springfield@example.org");
+        when(personService.getSignedInUser()).thenReturn(person);
+
+        final var department = department();
+        department.setMembers(List.of(person));
+        when(departmentService.getNumberOfDepartments()).thenReturn(1L);
+
+        when(vacationTypeService.getAllVacationTypes()).thenReturn(List.of());
+
+        when(workingTimeService.getFederalStatesByPersonAndDateRange(person, dateRange))
+            .thenReturn(Map.of(dateRange, GERMANY_BADEN_WUERTTEMBERG));
+
+        when(publicHolidaysService.getPublicHolidays(firstOfMonth, lastOfMonth, GERMANY_BADEN_WUERTTEMBERG))
+            .thenReturn(List.of(new PublicHoliday(december24, MORNING, "Heiligabend")));
+
+        final AbsencePeriod.RecordMorningPublicHoliday christmasEveMorning = new AbsencePeriod.RecordMorningPublicHoliday(person);
+        final AbsencePeriod.Record christmasEveRecord = new AbsencePeriod.Record(december24, person, christmasEveMorning, null);
+        final AbsencePeriod absencePeriod = new AbsencePeriod(List.of(christmasEveRecord));
+        when(absenceService.getOpenAbsences(List.of(person), firstOfMonth, lastOfMonth)).thenReturn(List.of(absencePeriod));
+
+        when(personService.getActivePersons()).thenReturn(List.of(person));
+
+        final ResultActions perform = perform(
+            get("/web/absences").locale(Locale.GERMANY)
+                .param("year", "2024")
+                .param("month", "12")
+        );
+        perform
+            .andExpect(status().isOk())
+            .andExpect(model().attribute("absenceOverview",
+                hasProperty("months", contains(
+                    hasProperty("persons", hasItem(
+                        hasProperty("days", hasItems(
+                            hasProperty("type",
+                                allOf(
+                                    hasProperty("absenceMorning", is(false)),
+                                    hasProperty("absenceNoon", is(false)),
+                                    hasProperty("absenceFull", is(false)),
+                                    hasProperty("publicHolidayMorning", is(true)),
+                                    hasProperty("publicHolidayNoon", is(false)),
+                                    hasProperty("publicHolidayFull", is(false))
+                                )
+                            )
+                        ))
+                    ))
+                ))
+            ));
+    }
+
+    @ParameterizedTest
+    @EnumSource(value = Role.class, names = {"BOSS", "OFFICE"})
+    void ensureChristmasEveOnNoonForPrivilegedPerson(Role role) throws Exception {
+
+        final LocalDate december24 = LocalDate.of(2024, DECEMBER, 24);
+        final LocalDate firstOfMonth = LocalDate.of(2024, DECEMBER, 1);
+        final LocalDate lastOfMonth = LocalDate.of(2024, DECEMBER, 31);
+        final DateRange dateRange = new DateRange(firstOfMonth, lastOfMonth);
+
+        final var person = new Person();
+        person.setId(1L);
+        person.setPermissions(List.of(USER, role));
+        person.setFirstName("Bruce");
+        person.setLastName("Springfield");
+        person.setEmail("springfield@example.org");
+        when(personService.getSignedInUser()).thenReturn(person);
+
+        final var department = department();
+        department.setMembers(List.of(person));
+        when(departmentService.getNumberOfDepartments()).thenReturn(1L);
+
+        when(vacationTypeService.getAllVacationTypes()).thenReturn(List.of());
+
+        when(workingTimeService.getFederalStatesByPersonAndDateRange(person, dateRange))
+            .thenReturn(Map.of(dateRange, GERMANY_BADEN_WUERTTEMBERG));
+
+        when(publicHolidaysService.getPublicHolidays(firstOfMonth, lastOfMonth, GERMANY_BADEN_WUERTTEMBERG))
+            .thenReturn(List.of(new PublicHoliday(december24, NOON, "Heiligabend")));
+
+        final AbsencePeriod.RecordNoonPublicHoliday christmasEveNoon = new AbsencePeriod.RecordNoonPublicHoliday(person);
+        final AbsencePeriod.Record christmasEveRecord = new AbsencePeriod.Record(december24, person, null, christmasEveNoon);
+        final AbsencePeriod absencePeriod = new AbsencePeriod(List.of(christmasEveRecord));
+
+        when(absenceService.getOpenAbsences(List.of(person), firstOfMonth, lastOfMonth)).thenReturn(List.of(absencePeriod));
+
+        when(personService.getActivePersons()).thenReturn(List.of(person));
+
+        perform(
+            get("/web/absences").locale(Locale.GERMANY)
+                .param("year", "2024")
+                .param("month", "12")
+        )
+            .andExpect(status().isOk())
+            .andExpect(model().attribute("absenceOverview",
+                hasProperty("months", contains(
+                    hasProperty("persons", hasItem(
+                        hasProperty("days", hasItems(
+                            hasProperty("type",
+                                allOf(
+                                    hasProperty("absenceMorning", is(false)),
+                                    hasProperty("absenceNoon", is(false)),
+                                    hasProperty("absenceFull", is(false)),
+                                    hasProperty("publicHolidayMorning", is(false)),
+                                    hasProperty("publicHolidayNoon", is(true)),
+                                    hasProperty("publicHolidayFull", is(false))
+                                )
+                            )
+                        ))
+                    ))
+                ))
+            ));
+    }
+
     @ParameterizedTest
     @EnumSource(value = Role.class, names = {"BOSS", "OFFICE"})
     void ensureAbsenceMorningAndSickNoteNoonForPrivilegedPerson(Role role) throws Exception {