diff --git a/library/Notifications/Widget/Calendar.php b/library/Notifications/Widget/Calendar.php index d75d271e3..327d31497 100644 --- a/library/Notifications/Widget/Calendar.php +++ b/library/Notifications/Widget/Calendar.php @@ -51,6 +51,9 @@ class Calendar extends BaseHtmlElement /** @var Url */ protected $addEntryUrl; + /** @var Url */ + protected $dayViewUrl; + public function setControls(Controls $controls): self { $this->controls = $controls; @@ -79,6 +82,19 @@ public function getAddEntryUrl(): ?Url return $this->addEntryUrl; } + public function setDayViewUrl(?Url $url): self + { + $this->dayViewUrl = $url; + + return $this; + } + + public function getDayViewUrl(string $date): ?Url + { + $url = clone $this->dayViewUrl; + return $url->setParam('day', $date); + } + protected function getModeStart(): DateTime { switch ($this->getControls()->getViewMode()) { @@ -91,7 +107,9 @@ protected function getModeStart(): DateTime return (new DateTime())->setTimestamp(strtotime($week)); default: - return DateTime::createFromFormat('Y-m-d', $this->getControls()->getValue('day')); + $day = $this->getControls()->getValue('day') ?: (new DateTime())->format('Y-m-d'); + + return DateTime::createFromFormat('Y-m-d H:i:s', $day . ' 00:00:00'); } } diff --git a/library/Notifications/Widget/Calendar/BaseGrid.php b/library/Notifications/Widget/Calendar/BaseGrid.php index fc48b0bac..5c9c03282 100644 --- a/library/Notifications/Widget/Calendar/BaseGrid.php +++ b/library/Notifications/Widget/Calendar/BaseGrid.php @@ -4,6 +4,7 @@ namespace Icinga\Module\Notifications\Widget\Calendar; +use DateInterval; use DateTime; use DateTimeInterface; use Icinga\Module\Notifications\Widget\Calendar; @@ -35,6 +36,9 @@ abstract class BaseGrid extends BaseHtmlElement /** @var DateTime */ protected $end; + /** @var array Extra counts stored as [date1 => count1, date2 => count2]*/ + protected $extraEntriesCount = []; + /** * Create a new calendar * @@ -73,6 +77,13 @@ abstract protected function calculateGridEnd(): DateTime; abstract protected function getNoOfVisuallyConnectedHours(): int; + /** + * Get Number of days in the grid + * + * @return int + */ + abstract protected function getNoOfDays(): int; + abstract protected function getGridArea(int $rowStart, int $rowEnd, int $colStart, int $colEnd): array; protected function getSectionsPerStep(): int @@ -137,6 +148,18 @@ protected function createGridOverlay(): BaseHtmlElement return $overlay; } + /** + * Fetch the count of additional number of entries for the given date + * + * @param string $date + * + * @return int + */ + public function getExtraEntryCount(string $date): int + { + return $this->extraEntriesCount[$date] ?? 0; + } + protected function assembleGridOverlay(BaseHtmlElement $overlay): void { $style = (new Style())->setNonce(Csp::getStyleNonce()); @@ -218,16 +241,12 @@ protected function assembleGridOverlay(BaseHtmlElement $overlay): void } } + $this->extraEntriesCount = []; foreach ($occupiedCells as $entry) { $continuation = false; $rows = $occupiedCells->getInfo(); foreach ($rows as $row => $hours) { list($rowStart, $rowSpan) = $rowPlacements[spl_object_id($entry)][$row]; - if ($rowStart > $row + $sectionsPerStep) { - // TODO: Register as +1 - continue; - } - $rowEnd = $rowStart + $rowSpan; $colStart = min($hours) + 1; $colEnd = max($hours) + 2; @@ -235,6 +254,26 @@ protected function assembleGridOverlay(BaseHtmlElement $overlay): void $gridArea = $this->getGridArea($rowStart, $rowEnd, $colStart, $colEnd); $entryClass = 'area-' . implode('-', $gridArea); + // Calculate number of entries that are not displayed in the grid for each date + if ($rowStart > $row + $sectionsPerStep) { + $stepRow = $rowStart - $this->getSectionsPerStep(); + $startOffset = $this->getStepOffset($stepRow, $gridArea[1] - 1); + $endOffset = $this->getStepOffset($stepRow, $gridArea[3] - 2); + $startDate = (clone $this->getGridStart())->add(new DateInterval("P$startOffset" . 'D')); + $duration = $endOffset - $startOffset; + for ($i = 0; $i <= $duration; $i++) { + $startDate->add(new DateInterval("P$i" . 'D')); + $countIdx = $startDate->format('Y-m-d'); + if (! isset($this->extraEntriesCount[$countIdx])) { + $this->extraEntriesCount[$countIdx] = 1; + } else { + $this->extraEntriesCount[$countIdx] += 1; + } + } + + continue; + } + $style->add(".$entryClass", [ 'grid-area' => sprintf('~"%d / %d / %d / %d"', ...$gridArea), 'background-color' => $entry->getAttendee()->getColor() . dechex((int) (256 * 0.1)), @@ -328,4 +367,26 @@ protected function roundToNearestThirtyMinute(DateTime $time): DateTime return $time; } + + /** + * Calculate offset of the day in the grid from given row and column + * + * @param int $row + * @param int $col + * + * @return int Offset of the day from grid start + */ + protected function getStepOffset(int $row, int $col): int + { + $days = $this->getNoOfDays(); + + switch ($days) { + case 7: // step offset for week mode + return intval($row / 4); + case 42: // step offset for month mode + return intval($row / $this->getSectionsPerStep()) * 7 + intval($col / 48); + default: // step offset for day mode + return 0; + } + } } diff --git a/library/Notifications/Widget/Calendar/DayGrid.php b/library/Notifications/Widget/Calendar/DayGrid.php index 277f88c32..435ffb5e7 100644 --- a/library/Notifications/Widget/Calendar/DayGrid.php +++ b/library/Notifications/Widget/Calendar/DayGrid.php @@ -6,6 +6,7 @@ use DateInterval; use DateTime; +use InvalidArgumentException; use ipl\Html\Attributes; use ipl\Html\BaseHtmlElement; use ipl\Html\HtmlElement; @@ -14,13 +15,20 @@ class DayGrid extends BaseGrid { - protected $mode = 'day'; - public function setGridStart(DateTime $start): BaseGrid { + if ($start->format('H:i:s') !== '00:00:00') { + throw new InvalidArgumentException('Start is not midnight'); + } + return parent::setGridStart($start); } + protected function getMaximumRowSpan(): int + { + return 28; + } + protected function calculateGridEnd(): DateTime { return (clone $this->getGridStart())->add(new DateInterval('P1D')); @@ -31,6 +39,11 @@ protected function getNoOfVisuallyConnectedHours(): int return 24; } + protected function getNoOfDays(): int + { + return 1; + } + protected function getGridArea(int $rowStart, int $rowEnd, int $colStart, int $colEnd): array { return [$colStart, $rowStart, $colEnd, $rowEnd]; @@ -59,7 +72,7 @@ protected function createHeader(): BaseHtmlElement new HtmlElement( 'span', Attributes::create(['class' => 'day-name']), - Text::create($this->getGridStart()->format('D')) + Text::create($currentDay->format('D')) ), new HtmlElement( 'span', diff --git a/library/Notifications/Widget/Calendar/ExtraEntryCount.php b/library/Notifications/Widget/Calendar/ExtraEntryCount.php new file mode 100644 index 000000000..11c4bfc11 --- /dev/null +++ b/library/Notifications/Widget/Calendar/ExtraEntryCount.php @@ -0,0 +1,63 @@ + 'extra-count']; + + /** @var ?Url Url to the day grid*/ + protected $url; + + /** @var BaseGrid */ + protected $grid; + + /** @var string Date in Y-m-d format */ + protected $date; + + /** + * Grid setter + * + * @param BaseGrid $grid + * + * @return $this + */ + public function setGrid(BaseGrid $grid): self + { + $this->grid = $grid; + + return $this; + } + + /** + * Setter for Date + * + * @param string $date Date in Y-m-d format + * + * @return $this + */ + public function setDate(string $date): self + { + $this->date = $date; + + return $this; + } + + protected function assemble() + { + $count = $this->grid->getExtraEntryCount($this->date); + if ($count > 0) { + $this->setContent($this->translatePlural( + "+$count entry", + "+$count entries", + $count + )); + } + } +} diff --git a/library/Notifications/Widget/Calendar/MonthGrid.php b/library/Notifications/Widget/Calendar/MonthGrid.php index e925cc1ae..a92c42ebc 100644 --- a/library/Notifications/Widget/Calendar/MonthGrid.php +++ b/library/Notifications/Widget/Calendar/MonthGrid.php @@ -28,6 +28,11 @@ public function setGridStart(DateTime $start): BaseGrid return parent::setGridStart($start); } + protected function getNoOfDays(): int + { + return 42; + } + protected function calculateGridEnd(): DateTime { return (clone $this->getGridStart())->add(new DateInterval('P42D')); @@ -36,6 +41,13 @@ protected function calculateGridEnd(): DateTime protected function assembleGridStep(BaseHtmlElement $content, DateTime $step): void { $content->addHtml(Text::create($step->format('j'))); + + $dayViewUrl = $this->calendar->getDayViewUrl($step->format('Y-m-d')); + $content->addHtml( + (new ExtraEntryCount(null, $dayViewUrl, ['target' => '_self'])) + ->setGrid($this) + ->setDate($step->format('Y-m-d')) + ); } protected function getRowStartModifier(): int diff --git a/library/Notifications/Widget/Calendar/WeekGrid.php b/library/Notifications/Widget/Calendar/WeekGrid.php index d26835b41..7c6d5b53d 100644 --- a/library/Notifications/Widget/Calendar/WeekGrid.php +++ b/library/Notifications/Widget/Calendar/WeekGrid.php @@ -34,6 +34,11 @@ protected function getNoOfVisuallyConnectedHours(): int return 24; } + protected function getNoOfDays(): int + { + return 7; + } + protected function getGridArea(int $rowStart, int $rowEnd, int $colStart, int $colEnd): array { return [$colStart, $rowStart, $colEnd, $rowEnd]; @@ -116,6 +121,18 @@ protected function createSidebar(): BaseHtmlElement return $sidebar; } + protected function assembleGridStep(BaseHtmlElement $content, DateTime $step): void + { + if ($step->format('H') === '23') { + $dayViewUrl = $this->calendar->getDayViewUrl($step->format('Y-m-d')); + $content->addHtml( + (new ExtraEntryCount(null, $dayViewUrl, ['target' => '_self'])) + ->setGrid($this) + ->setDate($step->format('Y-m-d')) + ); + } + } + protected function assemble() { $this->getAttributes()->add('class', 'week'); diff --git a/library/Notifications/Widget/Schedule.php b/library/Notifications/Widget/Schedule.php index 13bbed38c..6f992e9ba 100644 --- a/library/Notifications/Widget/Schedule.php +++ b/library/Notifications/Widget/Schedule.php @@ -5,6 +5,9 @@ namespace Icinga\Module\Notifications\Widget; use DateTimeZone; +use Icinga\Module\Notifications\Common\Database; +use Icinga\Module\Notifications\Model\ScheduleMember; +use Icinga\Module\Notifications\Model\TimeperiodEntry; use Icinga\Module\Notifications\Widget\Calendar\Attendee; use Icinga\Module\Notifications\Widget\Calendar\Controls; use Icinga\Module\Notifications\Widget\Calendar\Entry; @@ -44,47 +47,59 @@ protected function assembleCalendar(Calendar $calendar): void ['schedule' => $this->schedule->id] )); - $members = $this->schedule->member->with(['timeperiod', 'contact', 'contactgroup']); - foreach ($members as $member) { - if ($member->contact_id !== null) { - $attendee = new Attendee($member->contact->full_name); - $attendee->setColor($member->contact->color); - } else { // $member->contactgroup_id !== null - $attendee = new Attendee($member->contactgroup->name); - $attendee->setColor($member->contactgroup->color); - $attendee->setIcon('users'); - } + $calendar->setDayViewUrl(Url::fromPath( + 'notifications/schedules', + [ + 'mode' => 'day' + ] + )); - $entries = $member->timeperiod->entry; - - // TODO: This shouldn't be necessary. ipl/orm should be able to handle this by itself - $entries->setFilter(Filter::all(Filter::equal('timeperiod_id', $member->timeperiod->id))); - $entries->getSelectBase()->resetWhere(); - - $entryFilter = Filter::any( - Filter::all( // all entries that start in the shown range - Filter::greaterThanOrEqual('start_time', $calendar->getGrid()->getGridStart()->getTimestamp()), - Filter::lessThanOrEqual('start_time', $calendar->getGrid()->getGridEnd()->getTimestamp()) - ), - Filter::all( // all entries that end in the shown range - Filter::greaterThanOrEqual('end_time', $calendar->getGrid()->getGridStart()->getTimestamp()), - Filter::lessThanOrEqual('end_time', $calendar->getGrid()->getGridEnd()->getTimestamp()) - ), - Filter::all( // all entries that start before and end after the shown range - Filter::lessThanOrEqual('start_time', $calendar->getGrid()->getGridStart()->getTimestamp()), - Filter::greaterThanOrEqual('end_time', $calendar->getGrid()->getGridEnd()->getTimestamp()) - ), - Filter::none( // all entries that are repeated and may still occur in the shown range - Filter::lessThanOrEqual('until_time', $calendar->getGrid()->getGridStart()->getTimestamp()) - ), - Filter::all( // all entries that are repeated endlessly and already started in the past - Filter::unlike('until_time', '*'), - Filter::like('rrule', '*'), - Filter::lessThanOrEqual('start_time', $calendar->getGrid()->getGridStart()->getTimestamp()) - ) - ); + $db = Database::get(); + $entries = TimeperiodEntry::on($db) + ->orderBy(['start_time', 'timeperiod_id']); + + $entryFilter = Filter::any( + Filter::all( // all entries that start in the shown range + Filter::greaterThanOrEqual('start_time', $calendar->getGrid()->getGridStart()->getTimestamp()), + Filter::lessThanOrEqual('start_time', $calendar->getGrid()->getGridEnd()->getTimestamp()) + ), + Filter::all( // all entries that end in the shown range + Filter::greaterThanOrEqual('end_time', $calendar->getGrid()->getGridStart()->getTimestamp()), + Filter::lessThanOrEqual('end_time', $calendar->getGrid()->getGridEnd()->getTimestamp()) + ), + Filter::all( // all entries that start before and end after the shown range + Filter::lessThanOrEqual('start_time', $calendar->getGrid()->getGridStart()->getTimestamp()), + Filter::greaterThanOrEqual('end_time', $calendar->getGrid()->getGridEnd()->getTimestamp()) + ), + Filter::none( // all entries that are repeated and may still occur in the shown range + Filter::lessThanOrEqual('until_time', $calendar->getGrid()->getGridStart()->getTimestamp()) + ), + Filter::all( // all entries that are repeated endlessly and already started in the past + Filter::unlike('until_time', '*'), + Filter::like('rrule', '*'), + Filter::lessThanOrEqual('start_time', $calendar->getGrid()->getGridStart()->getTimestamp()) + ) + ); + + foreach ($entries->filter($entryFilter) as $entry) { + $members = ScheduleMember::on($db) + ->with(['timeperiod', 'contact', 'contactgroup']) + ->filter(Filter::all( + Filter::equal('schedule_id', $this->schedule->id), + Filter::equal('timeperiod_id', $entry->timeperiod_id) + )) + ->orderBy(['contact_id', 'contactgroup_id']); + + foreach ($members as $member) { + if ($member->contact_id !== null) { + $attendee = new Attendee($member->contact->full_name); + $attendee->setColor($member->contact->color); + } else { // $member->contactgroup_id !== null + $attendee = new Attendee($member->contactgroup->name); + $attendee->setColor($member->contactgroup->color); + $attendee->setIcon('users'); + } - foreach ($member->timeperiod->entry->filter($entryFilter) as $entry) { $calendar->addEntry( (new Entry($entry->id)) ->setDescription($entry->description) diff --git a/public/css/calendar.less b/public/css/calendar.less index 92156a562..7f8f5630a 100644 --- a/public/css/calendar.less +++ b/public/css/calendar.less @@ -71,6 +71,11 @@ } } + .overlay { + position: relative; + z-index: 1; + } + .entry { overflow: hidden; @@ -138,11 +143,16 @@ .step { grid-row-end: span @rowsPerDay; grid-column-end: span @columnsPerDay; + position: relative; > a { text-align: right; padding-right: .25em; } + + .extra-count { + height: auto; + } } } @@ -171,9 +181,25 @@ .step { grid-column-end: span @columnsPerDay; grid-row-end: span @rowsPerHour; + position: relative; + } + + .extra-count { + height: auto; } } +.extra-count { + z-index: 99; + background-color: @text-color-inverted; + border-radius: 0.25em; + position: absolute; + bottom: 0; + right: 0; + padding: 0 .25em; + color: @icinga-blue; +} + .calendar-grid.day { @days: 1; @hours: 24; @@ -219,6 +245,16 @@ .grid, .overlay { grid-area: ~"2 / 2 / 3 / 3"; + .entry-count { + display: flex; + flex-direction: row-reverse; + pointer-events: all; + white-space: nowrap; + + a { + color: @icinga-blue; + } + } } } @@ -281,6 +317,10 @@ > a { text-decoration: none; } + + .extra-count:hover { + text-decoration: underline; + } } .entry {