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

"Bruno Kambere \(@kambereBr\) via TikiWiki-cvs" <[email protected]>
Newsgroups gmane.comp.cms.tiki.cvs
Message-ID <69eb835c7e49e_3818ede0351d0@gitlab-sidekiq-low-urgency-cpu-bound-v2-998bf4d77-ttjbf.mail>

Bruno Kambere pushed to branch master at Tiki Wiki CMS Groupware / Tiki


Commits:
1beea816 by Bruno Kambere at 2026-04-24T17:42:49+03:00
[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

- - - - -


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/1beea81635b4b9ef6e393c2f19aab9468057ff76

-- 
View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/commit/1beea81635b4b9ef6e393c2f19aab9468057ff76
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.