[TikiWiki-commits] [Git][tikiwiki/tiki][30.x] [BP][FIX] CalendarController: Prevent one-day backward shift for date-only...

"Bruno Kambere \(@kambereBr\) via TikiWiki-cvs" <[email protected]>
Newsgroups gmane.comp.cms.tiki.cvs
Message-ID <69f09903f325c_3718ee4416386@gitlab-sidekiq-low-urgency-cpu-bound-v2-584f6cb8d4-2w7ss.mail>

Bruno Kambere pushed to branch 30.x at Tiki Wiki CMS Groupware / Tiki


Commits:
e72a4757 by Bruno Kambere at 2026-04-28T14:17:26+03:00
[BP][FIX] CalendarController: Prevent one-day backward shift for date-only tracker fields in non-UTC timezones
---
* [FIX] CalendarController: Prevent one-day backward shift for date-only tracker fields in non-UTC timezones
---
* [FIX] JsCalendar: Improve date normalization logic for form submissions

* [REF] Simplify date-only calendar field handling across picker and field handlers

* [ENH] CalendarController: Support TrackerList calendar with start-only/end-only date fields

* [REF] JsCalendar: Refactor normalizeDateOnlyTimestamp method

* [FIX] JsCalendar: Normalize date-only timestamps and improve date handling in date picker

* [ENH] CalendarController: Enhance date-only event handling for tracker fields

* [FIX] CalendarController: Prevent one-day backward shift for date-only tracker fields in non-UTC timezones

See merge request tikiwiki/tiki!9742

See merge request tikiwiki/tiki!10095

- - - - -


3 changed files:

- lib/core/Services/Tracker/CalendarController.php
- lib/core/Tracker/Field/DateTime.php
- lib/core/Tracker/Field/JsCalendar.php


Changes:

=====================================
lib/core/Services/Tracker/CalendarController.php
=====================================
@@ -24,17 +24,29 @@ class Services_Tracker_CalendarController
         $unifiedsearchlib = TikiLib::lib('unifiedsearch');
         $index = $unifiedsearchlib->getIndex();
 
-        $start = 'tracker_field_' . $input->beginField->word();
-        $end = 'tracker_field_' . $input->endField->word();
-        $title = 'tracker_field_' . $input->title->word();
-        $description = 'tracker_field_' . $input->description->word();
+        $beginFieldName = $input->beginField->word();
+        $endFieldName = $input->endField->word();
+        $start = 'tracker_field_' . $beginFieldName;
+        $end = 'tracker_field_' . $endFieldName;
+        $hasStartField = ! empty($beginFieldName) && $beginFieldName !== 'null';
+        $hasEndField = ! empty($endFieldName) && $endFieldName !== 'null';
+        $titleFieldName = $input->title->word();
+        $title = ($titleFieldName && $titleFieldName !== 'null') ? 'tracker_field_' . $titleFieldName : null;
+        $descriptionFieldName = $input->description->word();
+        $description = ($descriptionFieldName && $descriptionFieldName !== 'null') ? 'tracker_field_' . $descriptionFieldName : null;
 
-        if ($resource = $input->resourceField->word()) {
-            $resource = 'tracker_field_' . $resource;
+        $resource = null;
+        if ($resourceFieldName = $input->resourceField->word()) {
+            if ($resourceFieldName !== 'null') {
+                $resource = 'tracker_field_' . $resourceFieldName;
+            }
         }
 
-        if ($coloring = $input->coloringField->word()) {
-            $coloring = 'tracker_field_' . $coloring;
+        $coloring = null;
+        if ($coloringFieldName = $input->coloringField->word()) {
+            if ($coloringFieldName !== 'null') {
+                $coloring = 'tracker_field_' . $coloringFieldName;
+            }
         }
 
         $query = $unifiedsearchlib->buildQuery([]);
@@ -57,8 +69,12 @@ class Services_Tracker_CalendarController
             }
         }
 
-        $query->filterRange(0, $to, $start);
-        $query->filterRange($from, $to + 1000 * 365 * 86400, $end);
+        if ($hasStartField) {
+            $query->filterRange(0, $to, $start);
+        }
+        if ($hasEndField) {
+            $query->filterRange($from, $to + 1000 * 365 * 86400, $end);
+        }
         $maxRecords = $input->maxRecords->int() ?: null;
         $query->setRange(0, $maxRecords);
 
@@ -74,7 +90,25 @@ class Services_Tracker_CalendarController
         $response = [];
 
         $fields = [];
+        $beginDateFieldHandler = null;
+        $endDateFieldHandler = null;
         if ($definition = Tracker_Definition::get($input->trackerId->int())) {
+            $factory = $definition->getFieldFactory();
+
+            if ($hasStartField) {
+                $beginFieldInfo = $definition->getField($beginFieldName);
+                if ($beginFieldInfo) {
+                    $beginDateFieldHandler = $factory->getHandler($beginFieldInfo);
+                }
+            }
+
+            if ($hasEndField) {
+                $endFieldInfo = $definition->getField($endFieldName);
+                if ($endFieldInfo) {
+                    $endDateFieldHandler = $factory->getHandler($endFieldInfo);
+                }
+            }
+
             foreach ($definition->getPopupFields() as $fieldId) {
                 if ($field = $definition->getField($fieldId)) {
                     $fields[] = $field;
@@ -137,22 +171,53 @@ class Services_Tracker_CalendarController
 
             $colormap = base64_decode($input->colormap->word());
 
-            $dtStart = $this->getTimestamp($row[$start]);
-            $dtEnd = $this->getTimestamp($row[$end]);
+            $startValue = ($hasStartField && isset($row[$start])) ? $row[$start] : null;
+            $endValue = ($hasEndField && isset($row[$end])) ? $row[$end] : null;
+
+            if ($startValue === null && $endValue === null) {
+                continue;
+            }
+
+            // If only one of start or end is provided, use that value for both to ensure the event appears on the calendar,
+            $dtStart = $this->getTimestamp($startValue ?? $endValue);
+            $dtEnd = $this->getTimestamp($endValue ?? $startValue);
+
+            // If end is before start, treat as a single instant event by using the start value for both
+            if ($dtEnd < $dtStart) {
+                $dtEnd = $dtStart;
+            }
+
+            $beginIsDateOnly = $beginDateFieldHandler instanceof Tracker_Field_DateTime
+                && $beginDateFieldHandler->isDateOnlyCalendarValue();
+            $endIsDateOnly = $endDateFieldHandler instanceof Tracker_Field_DateTime
+                && $endDateFieldHandler->isDateOnlyCalendarValue();
+
+            // Determine if event is date-only
+            // If only one field is specified, check that field; if both are specified, both must be date-only
+            if ($beginDateFieldHandler !== null && $endDateFieldHandler !== null) {
+                $isDateOnlyEvent = $beginIsDateOnly && $endIsDateOnly;
+            } elseif ($beginDateFieldHandler !== null) {
+                $isDateOnlyEvent = $beginIsDateOnly;
+            } elseif ($endDateFieldHandler !== null) {
+                $isDateOnlyEvent = $endIsDateOnly;
+            } else {
+                $isDateOnlyEvent = false;
+            }
 
             $response[] = [
                 'id'               => $row['object_id'],
                 'trackerId'        => $row['tracker_id'] ?? null,
-                'title'            => $row[$title] ?: $row['title'],
-                'extendedProps'      => ['description' => $row[$description] ?: $row['description']],
+                'title'            => ($title && isset($row[$title])) ? $row[$title] : ($row['title'] ?? ''),
+                'extendedProps'      => ['description' => ($description && isset($row[$description])) ? $row[$description] : ($row['description'] ?? '')],
                 'url'              => smarty_modifier_sefurl($row['object_id'], $row['object_type']),
-                'allDay'           => false,
-                'start'            => $useTimestamp ? $dtStart : TikiLib::date_format("c", $dtStart, $user, 5, false),
-                'end'              => $useTimestamp ? $dtEnd : TikiLib::date_format("c", $dtEnd, $user, 5, false),
+                // For all-day events, return date-only strings so FullCalendar does not apply timezone conversions that can shift the visible day.
+                'allDay'           => $isDateOnlyEvent,
+                'start'            => $isDateOnlyEvent ? gmdate('Y-m-d', $dtStart) : ($useTimestamp ? $dtStart : TikiLib::date_format("c", $dtStart, $user, 5, false)),
+                'end'              => $isDateOnlyEvent ? gmdate('Y-m-d', $dtEnd) : ($useTimestamp ? $dtEnd : TikiLib::date_format("c", $dtEnd, $user, 5, false)),
                 'editable'         => $item->canModify(),
-                'color'            => $row[$coloring] ? ($row[$coloring] ?: $row['coloring']) : ($this->getColor($row[$coloring] ?? '', $colormap)),
+                'color'            => ($coloring && isset($row[$coloring])) ? ($row[$coloring] ?: $row['coloring'] ?? '') : ($this->getColor($row[$coloring] ?? '', $colormap)),
                 'textColor'        => '#000',
-                'resourceId'       => strtolower($row[$resource] ?? ''),
+                'resourceId'       => $resource && isset($row[$resource]) ? strtolower($row[$resource]) : '',
                 'resourceEditable' => true,
             ];
         }


=====================================
lib/core/Tracker/Field/DateTime.php
=====================================
@@ -441,4 +441,9 @@ class Tracker_Field_DateTime extends \Tracker\Field\AbstractItemField implements
             throw new Services_Exception(tr('Invalid UNIX timestamp "%0"', $value), 400);
         }
     }
+
+    public function isDateOnlyCalendarValue(): bool
+    {
+        return $this->getOption('datetime') === 'd';
+    }
 }


=====================================
lib/core/Tracker/Field/JsCalendar.php
=====================================
@@ -4,6 +4,9 @@
 //
 // All Rights Reserved. See copyright.txt for details and a complete list of authors.
 // Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
+
+use Tiki\Lib\TikiDate;
+
 class Tracker_Field_JsCalendar extends Tracker_Field_DateTime
 {
     /**
@@ -96,7 +99,8 @@ class Tracker_Field_JsCalendar extends Tracker_Field_DateTime
             $requestData[$ins_id] = $requestData[$ins_id]['date'];
         }
 
-        $value = (isset($requestData[$ins_id]))
+        $valueFromRequest = isset($requestData[$ins_id]);
+        $value = $valueFromRequest
             ? $requestData[$ins_id]
             : $this->getValue();
 
@@ -104,6 +108,14 @@ class Tracker_Field_JsCalendar extends Tracker_Field_DateTime
             try {
                 // prevent corrupted date values getting saved (e.g. from inline edit sometimes)
                 $this->validateTimestamp($value);
+
+                // Guard skips values already at UTC midnight. Only normalize when the value comes from a form submission, never from stored data.
+                if ($valueFromRequest && $this->getOption('datetime') === 'd' && is_numeric($value) && (int) $value % 86400 !== 0) {
+                    // Normalize to midnight UTC, mirroring DateTime handler (see DateTime::getFieldData).
+                    $timezone = $requestData[$ins_id . '_timezone'] ?? TikiLib::lib('tiki')->get_display_timezone();
+                    $server_offset = TikiDate::tzServerOffset($timezone, (int) $value);
+                    $value = (int) $value + $server_offset;
+                }
             } catch (Services_Exception $e) {
                 $value = '';
                 Feedback::error(tr('Date Picker Field: %0', $e->getMessage()));



View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/commit/e72a47573953aada82ac4aa1f678460fa28c1cc2

-- 
View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/commit/e72a47573953aada82ac4aa1f678460fa28c1cc2
You're receiving this email because of your account on gitlab.com. Manage all notifications: https://gitlab.com/-/profile/notifications | Help: https://gitlab.com/help

_______________________________________________
TikiWiki-cvs mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/tikiwiki-cvs
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.