diff --git a/app/Http/Controllers/CalendarController.php b/app/Http/Controllers/CalendarController.php index c176692..dc99977 100644 --- a/app/Http/Controllers/CalendarController.php +++ b/app/Http/Controllers/CalendarController.php @@ -52,6 +52,7 @@ class CalendarController extends Controller $defaultView = $user->getSetting('calendar.last_view', 'month'); $defaultDate = $user->getSetting('calendar.last_date', Carbon::today($tz)->toDateString()); $defaultDensity = (int) $user->getSetting('calendar.last_density', 30); + $defaultBusinessHours = (int) $user->getSetting('calendar.business_hours', 0); // week start preference $weekStartPref = $user->getSetting('calendar.week_start', 'sunday'); // 'sunday'|'monday' @@ -76,6 +77,16 @@ class CalendarController extends Controller 60 => 4, }; + // business hours toggle + $businessHoursEnabled = (int) $request->query('business_hours', $defaultBusinessHours) === 1; + $businessHoursRange = [ + 'start' => 8, + 'end' => 18, + ]; + $businessHoursRows = $businessHoursEnabled + ? intdiv((($businessHoursRange['end'] - $businessHoursRange['start']) * 60), 15) + : 96; + // date range span and controls $span = $this->gridSpan($view, $range, $weekStart, $weekEnd); $nav = $this->navDates($view, $range['start'], $tz); @@ -92,6 +103,9 @@ class CalendarController extends Controller $user->setSetting('calendar.last_date', $range['start']->toDateString()); $user->setSetting('calendar.last_density', (string) $stepMinutes); } + if ($request->has('business_hours')) { + $user->setSetting('calendar.business_hours', $businessHoursEnabled ? '1' : '0'); + } /** * @@ -121,6 +135,10 @@ class CalendarController extends Controller $span['end'] ); + $businessHoursForView = ($businessHoursEnabled && in_array($view, ['day', 'week', 'four'], true)) + ? $businessHoursRange + : null; + $events = $this->buildEventPayloads( $events, $calendar_map, @@ -130,6 +148,7 @@ class CalendarController extends Controller $tz, $recurrence, $span, + $businessHoursForView, ); /** @@ -175,6 +194,7 @@ class CalendarController extends Controller $tz, $recurrence, ['start' => $mini_grid_start, 'end' => $mini_grid_end], + null, ); // now build the mini from mini_events (not from $events) @@ -231,6 +251,12 @@ class CalendarController extends Controller 'mini' => $mini, // mini calendar days with events for indicators 'mini_nav' => $mini_nav, // separate mini calendar navigation 'mini_headers' => $mini_headers, + 'business_hours' => [ + 'enabled' => $businessHoursEnabled, + 'start' => $businessHoursRange['start'], + 'end' => $businessHoursRange['end'], + 'rows' => $businessHoursRows, + ], ]; // time-based payload values @@ -238,11 +264,23 @@ class CalendarController extends Controller if ($timeBased) { // create the time gutter if we're in a time-based view - $payload['slots'] = $this->timeSlots($range['start'], $tz, $timeFormat); + $payload['slots'] = $this->timeSlots( + $range['start'], + $tz, + $timeFormat, + $businessHoursEnabled ? $businessHoursRange : null + ); $payload['time_format'] = $timeFormat; // optional, if the blade cares // add the now indicator - $payload['now'] = $this->nowIndicator($view, $range, $tz, 15); + $payload['now'] = $this->nowIndicator( + $view, + $range, + $tz, + 15, + 1, + $businessHoursEnabled ? $businessHoursRange : null + ); } // send the density array always, even though it doesn't matter for month @@ -570,15 +608,23 @@ class CalendarController extends Controller * * Create the time gutter for time-based views */ - private function timeSlots(Carbon $dayStart, string $tz, string $timeFormat): array + private function timeSlots(Carbon $dayStart, string $tz, string $timeFormat, ?array $businessHours = null): array { $minutesPerSlot = 15; - $slotsPerDay = intdiv(24 * 60, $minutesPerSlot); // 96 + $startMinutes = 0; + $endMinutes = 24 * 60; + + if (is_array($businessHours)) { + $startMinutes = (int) $businessHours['start'] * 60; + $endMinutes = (int) $businessHours['end'] * 60; + } + + $slotsPerDay = intdiv(max(0, $endMinutes - $startMinutes), $minutesPerSlot); $format = $timeFormat === '24' ? 'H:i' : 'g:i a'; $slots = []; - $t = $dayStart->copy()->tz($tz)->startOfDay(); + $t = $dayStart->copy()->tz($tz)->startOfDay()->addMinutes($startMinutes); for ($i = 0; $i < $slotsPerDay; $i++) { $slots[] = [ @@ -586,7 +632,7 @@ class CalendarController extends Controller 'label' => $t->format($format), 'key' => $t->format('H:i'), // stable "machine" value 'index' => $i, // 0..95 - 'minutes' => $i * $minutesPerSlot, + 'minutes' => $startMinutes + ($i * $minutesPerSlot), 'duration' => $minutesPerSlot, // handy for styling math ]; @@ -613,7 +659,8 @@ class CalendarController extends Controller ?Carbon $endLocal, Carbon $rangeStart, string $view, - int $minutesPerSlot = 15 + int $minutesPerSlot = 15, + int $gridStartMinutes = 0 ): array { $start = $startLocal->copy(); @@ -626,7 +673,8 @@ class CalendarController extends Controller $displayMinutes = $durationMinutes > 0 ? $durationMinutes : $minutesPerSlot; // row placement (96 rows when minutesPerSlot=15) - $startMinutesFromMidnight = ($start->hour * 60) + $start->minute; + $startMinutesFromMidnight = (($start->hour * 60) + $start->minute) - $gridStartMinutes; + $startMinutesFromMidnight = max(0, $startMinutesFromMidnight); $startRow = intdiv($startMinutesFromMidnight, $minutesPerSlot) + 1; $rowSpan = max(1, (int) ceil($displayMinutes / $minutesPerSlot)); @@ -665,11 +713,14 @@ class CalendarController extends Controller array $range, string $tz, EventRecurrence $recurrence, - array $span + array $span, + ?array $businessHours = null ): Collection { $uiFormat = $timeFormat === '24' ? 'H:i' : 'g:ia'; $spanStartUtc = $span['start']->copy()->utc(); $spanEndUtc = $span['end']->copy()->utc(); + $gridStartMinutes = $businessHours ? ((int) $businessHours['start'] * 60) : 0; + $gridEndMinutes = $businessHours ? ((int) $businessHours['end'] * 60) : (24 * 60); return $events->flatMap(function ($e) use ( $calendarMap, @@ -679,7 +730,10 @@ class CalendarController extends Controller $tz, $recurrence, $spanStartUtc, - $spanEndUtc + $spanEndUtc, + $gridStartMinutes, + $gridEndMinutes, + $businessHours ) { $cal = $calendarMap[$e->calendarid]; $timezone = $cal->timezone ?? config('app.timezone'); @@ -723,7 +777,10 @@ class CalendarController extends Controller $tz, $timezone, $color, - $colorFg + $colorFg, + $gridStartMinutes, + $gridEndMinutes, + $businessHours ) { $startUtc = $occ['start']; $endUtc = $occ['end']; @@ -734,12 +791,28 @@ class CalendarController extends Controller $startForGrid = $startUtc->copy()->tz($tz); $endForGrid = $endUtc->copy()->tz($tz); + if ($businessHours) { + $startMinutes = ($startForGrid->hour * 60) + $startForGrid->minute; + $endMinutes = ($endForGrid->hour * 60) + $endForGrid->minute; + + if ($endMinutes <= $gridStartMinutes || $startMinutes >= $gridEndMinutes) { + return null; + } + + $displayStartMinutes = max($startMinutes, $gridStartMinutes); + $displayEndMinutes = min($endMinutes, $gridEndMinutes); + + $startForGrid = $startForGrid->copy()->startOfDay()->addMinutes($displayStartMinutes); + $endForGrid = $endForGrid->copy()->startOfDay()->addMinutes($displayEndMinutes); + } + $placement = $this->slotPlacement( $startForGrid, $endForGrid, $range['start']->copy()->tz($tz), $view, - 15 + 15, + $gridStartMinutes ); $occurrenceId = $occ['recurrence_id'] @@ -770,7 +843,7 @@ class CalendarController extends Controller 'start_col' => $placement['start_col'], 'duration' => $placement['duration'], ]; - }); + })->filter()->values(); })->keyBy('occurrence_id'); } @@ -967,7 +1040,14 @@ class CalendarController extends Controller * 'col_end' => int, // grid column end * ] */ - private function nowIndicator(string $view, array $range, string $tz, int $minutesPerSlot = 15, int $gutterCols = 1): array + private function nowIndicator( + string $view, + array $range, + string $tz, + int $minutesPerSlot = 15, + int $gutterCols = 1, + ?array $businessHours = null + ): array { // only meaningful for time-based views if (!in_array($view, ['day', 'week', 'four'], true)) { @@ -986,8 +1066,15 @@ class CalendarController extends Controller // row: minutes since midnight, snapped down to slot size $minutes = ($now->hour * 60) + $now->minute; - $snapped = intdiv($minutes, $minutesPerSlot) * $minutesPerSlot; - $row = intdiv($snapped, $minutesPerSlot) + 1; // 1-based + $gridStartMinutes = $businessHours ? ((int) $businessHours['start'] * 60) : 0; + $gridEndMinutes = $businessHours ? ((int) $businessHours['end'] * 60) : (24 * 60); + + if ($businessHours && ($minutes < $gridStartMinutes || $minutes > $gridEndMinutes)) { + return ['show' => false, 'row' => 1, 'day_col' => 1, 'col_start' => 1, 'col_end' => 2]; + } + + $snapped = intdiv($minutes - $gridStartMinutes, $minutesPerSlot) * $minutesPerSlot; + $row = intdiv(max(0, $snapped), $minutesPerSlot) + 1; // 1-based // column: 1..N where 1 is the first day column in the events grid if ($view === 'day') { diff --git a/resources/css/lib/calendar.css b/resources/css/lib/calendar.css index 952d63b..b69878f 100644 --- a/resources/css/lib/calendar.css +++ b/resources/css/lib/calendar.css @@ -131,7 +131,7 @@ /* time column */ ol.time { @apply grid z-0 pt-4; - grid-template-rows: repeat(96, var(--row-height)); + grid-template-rows: repeat(var(--grid-rows, 96), var(--row-height)); time { @apply relative flex items-center justify-end items-start pr-4; @@ -150,7 +150,7 @@ /* event positioning */ ol.events { @apply grid py-4; - grid-template-rows: repeat(96, var(--row-height)); + grid-template-rows: repeat(var(--grid-rows, 96), var(--row-height)); --event-col: 0; --event-row: 0; --event-end: 4; @@ -381,4 +381,3 @@ transform: translateX(0); } } - diff --git a/resources/views/calendar/index.blade.php b/resources/views/calendar/index.blade.php index 547c7ac..fde170c 100644 --- a/resources/views/calendar/index.blade.php +++ b/resources/views/calendar/index.blade.php @@ -74,6 +74,7 @@ :view="$view" :density="$density" :headers="$mini_headers" + :business_hours="$business_hours" class="aside-inset" /> @@ -105,6 +106,7 @@ {{-- persist values from other forms --}} +