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 --}}
+