Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
7eb27f2
Remove timeline from schedule list item
jrauh01 Oct 13, 2025
1de3675
Add timezone column to schedule model
jrauh01 Oct 14, 2025
1ad0885
Schedule creation form: add timezone dropdown
jrauh01 Oct 13, 2025
125f32e
Add `TimezonePicker` control
jrauh01 Oct 17, 2025
eedc1ca
Add `TimezonePicker` to `ScheduleController`
jrauh01 Oct 17, 2025
1982544
Add `ScheduleDateTimeFactory`
jrauh01 Oct 17, 2025
2dfbe23
Set timezone for `ScheduleDateTimeFactory`
jrauh01 Oct 17, 2025
d2e40b7
Display schedule in chosen timezone
jrauh01 Oct 17, 2025
1d28930
Add `TimezoneWarning`
jrauh01 Oct 17, 2025
c9b6793
Add `TimezoneWarning` to the modals...
jrauh01 Oct 17, 2025
f9af3ec
Use schedule timezone for `RotationConfigForm`
jrauh01 Oct 17, 2025
cb81322
Add times for the next day to own option group
jrauh01 Oct 17, 2025
382436d
Show times for display timezone
jrauh01 Oct 17, 2025
03ff28f
Show schedule timezone in first handoff hint ...
jrauh01 Oct 27, 2025
7915a84
Keep display timezone on drag & drop
jrauh01 Oct 28, 2025
bd0609c
Adjust timezones for experimental parts
jrauh01 Oct 28, 2025
dd30d8a
Use `FormUid` for controls ...
jrauh01 Oct 30, 2025
9f79515
Replace `ScheduleDateTimeFactory` ...
jrauh01 Oct 30, 2025
7785f40
Use display timezone in rotation ...
jrauh01 Oct 30, 2025
2e292c9
Remove extra schedule query in `RotationConfigForm`
jrauh01 Oct 30, 2025
8548637
Change timezone source for `TimezonePicker`
jrauh01 Nov 3, 2025
e2c7a27
Select schedule timezone via suggestion element
jrauh01 Nov 3, 2025
79bffe6
Add first handoff hint in the display timezone
jrauh01 Nov 5, 2025
8507064
Remove times for display timezone
jrauh01 Nov 5, 2025
61ae8e5
Use session for display timezone
jrauh01 Nov 5, 2025
62b216b
Use `RRule->setTimezone()`
jrauh01 Nov 5, 2025
b0cef34
Fix flyout weekday
jrauh01 Nov 6, 2025
bf12f9f
Change the way flyouts get timezones
jrauh01 Nov 6, 2025
8146c8c
Use timeperiod entry timezone for `RRule`
jrauh01 Nov 6, 2025
b62d1b2
Remove `ScheduleTimezoneStorage`
jrauh01 Nov 6, 2025
ff39b69
Fix `TimezonePicker` css classes
jrauh01 Nov 6, 2025
d988517
Move session handling to `ScheduleController`
jrauh01 Nov 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 100 additions & 5 deletions application/controllers/ScheduleController.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,19 @@

namespace Icinga\Module\Notifications\Controllers;

use DateTime;
use DateTimeZone;
use Icinga\Module\Notifications\Common\Database;
use Icinga\Module\Notifications\Common\Links;
use Icinga\Module\Notifications\Forms\MoveRotationForm;
use Icinga\Module\Notifications\Forms\RotationConfigForm;
use Icinga\Module\Notifications\Forms\ScheduleForm;
use Icinga\Module\Notifications\Model\Schedule;
use Icinga\Module\Notifications\Widget\RecipientSuggestions;
use Icinga\Module\Notifications\Web\Control\TimezonePicker;
use Icinga\Module\Notifications\Widget\Detail\ScheduleDetail;
use Icinga\Module\Notifications\Widget\RecipientSuggestions;
use Icinga\Module\Notifications\Widget\TimezoneWarning;
use Icinga\Web\Session;
use ipl\Html\Form;
use ipl\Html\Html;
use ipl\Stdlib\Filter;
Expand Down Expand Up @@ -55,8 +60,18 @@ public function indexAction(): void
})
->handleRequest($this->getServerRequest());

$timezonePicker = $this->createTimezonePicker($schedule->timezone, $id);

$this->addControl($timezonePicker);
$this->addControl($scheduleControls);
$this->addContent(new ScheduleDetail($schedule, $scheduleControls));
$this->addContent(new ScheduleDetail(
$schedule,
$scheduleControls,
new DateTime('today', new DateTimeZone(
$timezonePicker->getValue(TimezonePicker::DEFAULT_TIMEZONE_PARAM)
?? $this->getDisplayTimezoneFromSession($schedule->timezone)
)),
));
}

public function settingsAction(): void
Expand Down Expand Up @@ -92,7 +107,8 @@ public function addAction(): void
{
$this->setTitle($this->translate('New Schedule'));
$form = (new ScheduleForm(Database::get()))
->setAction($this->getRequest()->getUrl()->getAbsoluteUrl())
->setShowTimezoneSuggestionInput()
->setAction($this->getRequest()->getUrl()->setParam('showCompact')->getAbsoluteUrl())
->on(Form::ON_SUCCESS, function (ScheduleForm $form) {
$scheduleId = $form->addSchedule();

Expand All @@ -108,9 +124,15 @@ public function addAction(): void
public function addRotationAction(): void
{
$scheduleId = (int) $this->params->getRequired('schedule');
$scheduleTimezone = $this->getScheduleTimezone($scheduleId);
$displayTimezone = $this->getDisplayTimezoneFromSession($scheduleTimezone);
$this->setTitle($this->translate('Add Rotation'));

$form = new RotationConfigForm($scheduleId, Database::get());
if ($displayTimezone !== $scheduleTimezone) {
$this->addContent(new TimezoneWarning($scheduleTimezone));
}

$form = new RotationConfigForm($scheduleId, Database::get(), $displayTimezone, $scheduleTimezone);
$form->setAction($this->getRequest()->getUrl()->setParam('showCompact')->getAbsoluteUrl());
$form->setSuggestionUrl(Url::fromPath('notifications/schedule/suggest-recipient'));
$form->on(RotationConfigForm::ON_SENT, function ($form) {
Expand Down Expand Up @@ -141,9 +163,15 @@ public function editRotationAction(): void
{
$id = (int) $this->params->getRequired('id');
$scheduleId = (int) $this->params->getRequired('schedule');
$scheduleTimezone = $this->getScheduleTimezone($scheduleId);
$displayTimezone = $this->getDisplayTimezoneFromSession($scheduleTimezone);
$this->setTitle($this->translate('Edit Rotation'));

$form = new RotationConfigForm($scheduleId, Database::get());
if ($displayTimezone !== $scheduleTimezone) {
$this->addContent(new TimezoneWarning($scheduleTimezone));
}

$form = new RotationConfigForm($scheduleId, Database::get(), $displayTimezone, $scheduleTimezone);
$form->disableModeSelection();
$form->setShowRemoveButton();
$form->loadRotation($id);
Expand Down Expand Up @@ -204,4 +232,71 @@ public function suggestRecipientAction(): void

$this->getDocument()->addHtml($suggestions);
}

/**
* Get the timezone of a schedule
*
* @param int $scheduleId The ID of the schedule
*
* @return string The timezone of the schedule
*/
protected function getScheduleTimezone(int $scheduleId): string
{
return Schedule::on(Database::get())
->filter(Filter::equal('schedule.id', $scheduleId))
->first()
->timezone;
}

/**
* Create a timezone picker control
*
* @param string $scheduleTimezone The schedule timezone is used as default if no timezone is in the session
* @param int $scheduleId The schedule id
*
* @return TimezonePicker The timezone picker control
*/
protected function createTimezonePicker(string $scheduleTimezone, int $scheduleId): TimezonePicker
{
$defaultTimezoneParam = TimezonePicker::DEFAULT_TIMEZONE_PARAM;
return (new TimezonePicker())
->populate([
$defaultTimezoneParam => $this->params->get($defaultTimezoneParam)
?? $this->getDisplayTimezoneFromSession($scheduleTimezone)
])
->on(
TimezonePicker::ON_SUBMIT,
function (TimezonePicker $timezonePicker) use ($defaultTimezoneParam, $scheduleId, $scheduleTimezone) {
$this->writeDisplayTimezoneToSession($timezonePicker->getValue($defaultTimezoneParam));
$this->redirectNow(Links::schedule($scheduleId)->with([
$defaultTimezoneParam => $timezonePicker->getValue($defaultTimezoneParam)
]));
}
)
->handleRequest($this->getServerRequest());
}

/**
* Get the display timezone from the session
*
* @param string|null $defaultTimezone
*
* @return string
*/
protected function getDisplayTimezoneFromSession(?string $defaultTimezone = null): string
{
return Session::getSession()->getNamespace('notifications')->get('schedule.display_timezone', $defaultTimezone);
}

/**
* Write the display timezone to the session
*
* @param string $displayTimezone
*
* @return void
*/
protected function writeDisplayTimezoneToSession(string $displayTimezone): void
{
Session::getSession()->getNamespace('notifications')->set('schedule.display_timezone', $displayTimezone);
}
}
10 changes: 0 additions & 10 deletions application/controllers/SchedulesController.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,12 @@

namespace Icinga\Module\Notifications\Controllers;

use DateTime;
use Icinga\Module\Notifications\Common\Database;
use Icinga\Module\Notifications\Common\Links;
use Icinga\Module\Notifications\Model\Schedule;
use Icinga\Module\Notifications\View\ScheduleRenderer;
use Icinga\Module\Notifications\Web\Control\SearchBar\ObjectSuggestions;
use Icinga\Module\Notifications\Widget\ItemList\ObjectList;
use Icinga\Module\Notifications\Widget\TimeGrid\DaysHeader;
use ipl\Html\Attributes;
use ipl\Html\HtmlElement;
use ipl\Stdlib\Filter;
use ipl\Web\Compat\CompatController;
use ipl\Web\Compat\SearchControls;
Expand Down Expand Up @@ -78,12 +74,6 @@ public function indexAction(): void
))->openInModal()
);

$this->addContent(new HtmlElement(
'div',
Attributes::create(['class' => 'schedules-header']),
new DaysHeader((new DateTime())->setTime(0, 0), 7)
));

$this->addContent(new ObjectList($schedules, new ScheduleRenderer()));

if (! $searchBar->hasBeenSubmitted() && $searchBar->hasBeenSent()) {
Expand Down
35 changes: 35 additions & 0 deletions application/controllers/SuggestController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

/* Icinga Notifications Web | (c) 2025 Icinga GmbH | GPLv2 */

namespace Icinga\Module\Notifications\Controllers;

use DateTime;
use DateTimeZone;
use IntlTimeZone;
use ipl\Web\Compat\CompatController;
use ipl\Web\FormElement\SearchSuggestions;
use Throwable;

class SuggestController extends CompatController
{
public function timezoneAction(): void
{
$suggestions = new SearchSuggestions((function () use (&$suggestions) {
foreach (IntlTimeZone::createEnumeration() as $tz) {
try {
if (
(new DateTime('now', new DateTimeZone($tz)))->getTimezone()->getLocation()
&& $suggestions->matchSearch($tz)
) {
yield ['search' => $tz];
}
} catch (Throwable) {
continue;
}
}
})());

$this->getDocument()->addHtml($suggestions->forRequest($this->getServerRequest()));
}
}
75 changes: 62 additions & 13 deletions application/forms/RotationConfigForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use DateInterval;
use DateTime;
use DateTimeZone;
use Generator;
use Icinga\Exception\ConfigurationError;
use Icinga\Exception\Http\HttpNotFoundException;
Expand Down Expand Up @@ -80,6 +81,12 @@ class RotationConfigForm extends CompatForm
/** @var int The rotation id */
protected $rotationId;

/** @var string The timezone to display the timeline in */
protected $displayTimezone;

/** @var string The timezone the schedule is created in */
protected $scheduleTimezone;

/**
* Set the label for the submit button
*
Expand Down Expand Up @@ -187,11 +194,15 @@ public function hasBeenWiped(): bool
*
* @param int $scheduleId
* @param Connection $db
* @param string $displayTimezone
* @param string $scheduleTimezone
*/
public function __construct(int $scheduleId, Connection $db)
public function __construct(int $scheduleId, Connection $db, string $displayTimezone, string $scheduleTimezone)
{
$this->db = $db;
$this->scheduleId = $scheduleId;
$this->displayTimezone = $displayTimezone;
$this->scheduleTimezone = $scheduleTimezone;

$this->applyDefaultElementDecorators();
}
Expand Down Expand Up @@ -227,7 +238,11 @@ public function loadRotation(int $rotationId): self
throw new LogicException('Invalid mode');
}

$handoff = DateTime::createFromFormat('Y-m-d H:i', $rotation->first_handoff . ' ' . $time);
$handoff = DateTime::createFromFormat(
'Y-m-d H:i',
$rotation->first_handoff . ' ' . $time,
new DateTimeZone($this->scheduleTimezone)
);
if ($handoff === false) {
throw new ConfigurationError('Invalid date format');
}
Expand Down Expand Up @@ -258,7 +273,9 @@ public function loadRotation(int $rotationId): self
->orderBy('until_time', SORT_DESC)
->first();
if ($previousShift !== null) {
$this->previousShift = $previousShift->until_time;
$this->previousShift = $previousShift->until_time->setTimezone(
new DateTimeZone($this->scheduleTimezone)
);
}

/** @var ?Rotation $newerRotation */
Expand Down Expand Up @@ -452,6 +469,9 @@ public function editRotation(int $rotationId): void
->filter(Filter::equal('timeperiod.owned_by_rotation_id', $rotationId));

foreach ($timeperiodEntries as $timeperiodEntry) {
$timeperiodEntry->start_time->setTimezone(new DateTimeZone($this->scheduleTimezone));
$timeperiodEntry->end_time->setTimezone(new DateTimeZone($this->scheduleTimezone));

/** @var TimeperiodEntry $timeperiodEntry */
$rrule = $timeperiodEntry->toRecurrenceRule();
$shiftDuration = $timeperiodEntry->start_time->diff($timeperiodEntry->end_time);
Expand Down Expand Up @@ -819,9 +839,10 @@ protected function assemblePartialDayOptions(FieldsetElement $options): DateTime
->replaceDecorator('Description', DescriptionDecorator::class, ['class' => 'description']);

$selectedFromTime = $from->getValue();
$nextDayTimeOptions = [];
foreach ($timeOptions as $key => $value) {
unset($timeOptions[$key]); // unset to re-add it at the end of array
$timeOptions[$key] = sprintf('%s (%s)', $value, $this->translate('Next Day'));
unset($timeOptions[$key]);
$nextDayTimeOptions[$key] = $value;

if ($selectedFromTime === $key) {
break;
Expand All @@ -830,7 +851,9 @@ protected function assemblePartialDayOptions(FieldsetElement $options): DateTime

$to = $options->createElement('select', 'to', [
'required' => true,
'options' => $timeOptions
'options' => empty($timeOptions)
? [$this->translate('Next Day') => $nextDayTimeOptions]
: [$this->translate('Same Day') => $timeOptions, $this->translate('Next Day') => $nextDayTimeOptions]
]);
$options->registerElement($to);

Expand Down Expand Up @@ -1226,11 +1249,35 @@ function ($value, $validator) use ($earliestHandoff, $firstHandoff, $latestHando
(new \IntlDateFormatter(
\Locale::getDefault(),
\IntlDateFormatter::MEDIUM,
\IntlDateFormatter::SHORT
\IntlDateFormatter::SHORT,
$this->scheduleTimezone
))->format($actualFirstHandoff)
);
}
})
}),
new HtmlElement('br'),
$this->displayTimezone !== $this->scheduleTimezone ? DeferredText::create(function () {
$ruleGenerator = $this->yieldRecurrenceRules(1);
if (! $ruleGenerator->valid()) {
return '';
}

$actualFirstHandoff = $ruleGenerator->current()[0]->getStartDate();
if ($actualFirstHandoff < new DateTime()) {
return '';
} else {
return sprintf(
$this->translate('In your chosen display timezone (%s) this is the %s'),
$this->displayTimezone,
(new \IntlDateFormatter(
\Locale::getDefault(),
\IntlDateFormatter::MEDIUM,
\IntlDateFormatter::SHORT,
$this->displayTimezone
))->format($actualFirstHandoff)
);
}
}) : new HtmlDocument()
));
}

Expand Down Expand Up @@ -1293,12 +1340,13 @@ private function parseDateAndTime(?string $date = null, ?string $time = null): D
}

if (! $format) {
return (new DateTime())->setTime(0, 0);
return new DateTime('today', new DateTimeZone($this->scheduleTimezone));
}

$datetime = DateTime::createFromFormat($format, $expression);
$datetime = DateTime::createFromFormat($format, $expression, new DateTimeZone($this->scheduleTimezone));

if ($datetime === false) {
$datetime = (new DateTime())->setTime(0, 0);
$datetime = new DateTime('today', $this->scheduleTimezone);
} elseif ($time === null) {
$datetime->setTime(0, 0);
}
Expand All @@ -1316,11 +1364,12 @@ private function getTimeOptions(): array
$formatter = new \IntlDateFormatter(
\Locale::getDefault(),
\IntlDateFormatter::NONE,
\IntlDateFormatter::SHORT
\IntlDateFormatter::SHORT,
$this->scheduleTimezone
);

$options = [];
$dt = new DateTime();
$dt = new DateTime('now', new DateTimeZone($this->scheduleTimezone));
for ($hour = 0; $hour < 24; $hour++) {
for ($minute = 0; $minute < 60; $minute += 30) {
$dt->setTime($hour, $minute);
Expand Down
Loading
Loading