From 3726ba240444b828790c733b0186a57edd8502af Mon Sep 17 00:00:00 2001 From: raviks789 <33730024+raviks789@users.noreply.github.com> Date: Fri, 22 Sep 2023 17:38:34 +0200 Subject: [PATCH] Show extra appointments link for week and month views --- library/Notifications/Model/Timeperiod.php | 2 +- library/Notifications/Widget/Calendar.php | 23 ++++- .../Widget/Calendar/BaseGrid.php | 47 ++++++++-- .../Notifications/Widget/Calendar/DayGrid.php | 23 ++++- .../Widget/Calendar/ExtraEntryCount.php | 65 ++++++++++++++ .../Widget/Calendar/MonthGrid.php | 7 ++ .../Widget/Calendar/WeekGrid.php | 12 +++ library/Notifications/Widget/Schedule.php | 89 +++++++++++-------- public/css/calendar.less | 40 +++++++++ 9 files changed, 259 insertions(+), 49 deletions(-) create mode 100644 library/Notifications/Widget/Calendar/ExtraEntryCount.php diff --git a/library/Notifications/Model/Timeperiod.php b/library/Notifications/Model/Timeperiod.php index 0484b9276..26861bebe 100644 --- a/library/Notifications/Model/Timeperiod.php +++ b/library/Notifications/Model/Timeperiod.php @@ -36,7 +36,7 @@ public function createRelations(Relations $relations) $relations->hasOne('parent', Schedule::class) ->setCandidateKey('owned_by_schedule_id') ->setJoinType('LEFT'); - $relations->hasMany('entry', TimeperiodEntry::class) + $relations->hasMany('timeperiod_entry', TimeperiodEntry::class) ->setJoinType('LEFT'); } } diff --git a/library/Notifications/Widget/Calendar.php b/library/Notifications/Widget/Calendar.php index d75d271e3..0e1a52286 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 $url; + public function setControls(Controls $controls): self { $this->controls = $controls; @@ -79,6 +82,22 @@ public function getAddEntryUrl(): ?Url return $this->addEntryUrl; } + public function setUrl(?Url $url): self + { + $this->url = $url; + + return $this; + } + + public function prepareDayViewUrl(DateTime $date): ?Url + { + $url = clone $this->url; + return $url->overwriteParams([ + 'mode' => 'day', + 'day' => $date->format('Y-m-d') + ]); + } + protected function getModeStart(): DateTime { switch ($this->getControls()->getViewMode()) { @@ -91,7 +110,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..7c487dca3 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 * @@ -137,6 +141,18 @@ protected function createGridOverlay(): BaseHtmlElement return $overlay; } + /** + * Fetch the count of additional number of entries for the given date + * + * @param DateTime $date + * + * @return int + */ + public function getExtraEntryCount(DateTime $date): int + { + return $this->extraEntriesCount[$date->format('Y-m-d')] ?? 0; + } + protected function assembleGridOverlay(BaseHtmlElement $overlay): void { $style = (new Style())->setNonce(Csp::getStyleNonce()); @@ -218,21 +234,42 @@ 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]; + $colStart = min($hours); + $colEnd = max($hours); + + // Calculate number of entries that are not displayed in the grid for each date if ($rowStart > $row + $sectionsPerStep) { - // TODO: Register as +1 + $startOffset = (int) (($row / $sectionsPerStep) * ($gridBorderAt / 48) + $colStart / 48); + $endOffset = (int) (($row / $sectionsPerStep) * ($gridBorderAt / 48) + $colEnd / 48); + $startDate = (clone $this->getGridStart())->add(new DateInterval("P$startOffset" . 'D')); + $duration = $endOffset - $startOffset; + for ($i = 0; $i <= $duration; $i++) { + $countIdx = $startDate->format('Y-m-d'); + if (! isset($this->extraEntriesCount[$countIdx])) { + $this->extraEntriesCount[$countIdx] = 1; + } else { + $this->extraEntriesCount[$countIdx] += 1; + } + + $startDate->add(new DateInterval('P1D')); + } + continue; } - $rowEnd = $rowStart + $rowSpan; - $colStart = min($hours) + 1; - $colEnd = max($hours) + 2; + $gridArea = $this->getGridArea( + $rowStart, + $rowStart + $rowSpan, + $colStart + 1, + $colEnd + 2 + ); - $gridArea = $this->getGridArea($rowStart, $rowEnd, $colStart, $colEnd); $entryClass = 'area-' . implode('-', $gridArea); $style->add(".$entryClass", [ diff --git a/library/Notifications/Widget/Calendar/DayGrid.php b/library/Notifications/Widget/Calendar/DayGrid.php index 277f88c32..c1c56d926 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')); @@ -50,6 +58,15 @@ protected function createGridSteps(): Traversable protected function createHeader(): BaseHtmlElement { $header = new HtmlElement('div', Attributes::create(['class' => 'header'])); + $dayNames = [ + 'Mon' => t('Mon', 'monday'), + 'Tue' => t('Tue', 'tuesday'), + 'Wed' => t('Wed', 'wednesday'), + 'Thu' => t('Thu', 'thursday'), + 'Fri' => t('Fri', 'friday'), + 'Sat' => t('Sat', 'saturday'), + 'Sun' => t('Sun', 'sunday') + ]; $currentDay = clone $this->getGridStart(); $interval = new DateInterval('P1D'); @@ -59,7 +76,7 @@ protected function createHeader(): BaseHtmlElement new HtmlElement( 'span', Attributes::create(['class' => 'day-name']), - Text::create($this->getGridStart()->format('D')) + Text::create($dayNames[$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..8ac2000a7 --- /dev/null +++ b/library/Notifications/Widget/Calendar/ExtraEntryCount.php @@ -0,0 +1,65 @@ + 'extra-count', 'target' => '_self']; + + /** @var BaseGrid */ + protected $grid; + + /** @var DateTime Day for which the extra count is being registered */ + protected $gridStep; + + /** + * Set the grid the tied to this extra count + * + * @param BaseGrid $grid + * + * @return $this + */ + public function setGrid(BaseGrid $grid): self + { + $this->grid = $grid; + + return $this; + } + + /** + * Set the day for which the extra count is being registered + * + * @param DateTime $gridStep Grid step + * + * @return $this + */ + public function setGridStep(DateTime $gridStep): self + { + $this->gridStep = clone $gridStep; + + return $this; + } + + protected function assemble() + { + $count = $this->grid->getExtraEntryCount($this->gridStep); + if ($count > 0) { + $this->setContent( + sprintf( + $this->translatePlural( + '+%d entry', + '+%d entries', + $count + ), + $count + ) + ); + } + } +} diff --git a/library/Notifications/Widget/Calendar/MonthGrid.php b/library/Notifications/Widget/Calendar/MonthGrid.php index e925cc1ae..d53ee06b9 100644 --- a/library/Notifications/Widget/Calendar/MonthGrid.php +++ b/library/Notifications/Widget/Calendar/MonthGrid.php @@ -36,6 +36,13 @@ protected function calculateGridEnd(): DateTime protected function assembleGridStep(BaseHtmlElement $content, DateTime $step): void { $content->addHtml(Text::create($step->format('j'))); + + $dayViewUrl = $this->calendar->prepareDayViewUrl($step); + $content->addHtml( + (new ExtraEntryCount(null, $dayViewUrl)) + ->setGrid($this) + ->setGridStep($step) + ); } protected function getRowStartModifier(): int diff --git a/library/Notifications/Widget/Calendar/WeekGrid.php b/library/Notifications/Widget/Calendar/WeekGrid.php index d26835b41..7e298fbf3 100644 --- a/library/Notifications/Widget/Calendar/WeekGrid.php +++ b/library/Notifications/Widget/Calendar/WeekGrid.php @@ -116,6 +116,18 @@ protected function createSidebar(): BaseHtmlElement return $sidebar; } + protected function assembleGridStep(BaseHtmlElement $content, DateTime $step): void + { + if ($step->format('H') === '23') { + $dayViewUrl = $this->calendar->prepareDayViewUrl($step); + $content->addHtml( + (new ExtraEntryCount(null, $dayViewUrl)) + ->setGrid($this) + ->setGridStep($step) + ); + } + } + protected function assemble() { $this->getAttributes()->add('class', 'week'); diff --git a/library/Notifications/Widget/Schedule.php b/library/Notifications/Widget/Schedule.php index 13bbed38c..abfdd3b1b 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,55 @@ 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->setUrl(Url::fromPath( + 'notifications/schedules', + ['schedule' => $this->schedule->id] + )); - $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) + ->filter(Filter::equal('timeperiod.schedule.id', $this->schedule->id)) + ->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::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 {