[TikiWiki-commits] [Git][tikiwiki/tiki][29.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 <69f0d44ce4b61_3818ede06276@gitlab-sidekiq-low-urgency-cpu-bound-v2-77c9576fd9-cc54n.mail>

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


Commits:
d545d8b4 by Bruno Kambere at 2026-04-28T18:29:01+03:00
[BP][FIX] CalendarController: Prevent one-day backward shift for date-only tracker fields in non-UTC timezones
---
* [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

See merge request tikiwiki/tiki!10097

- - - - -


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;
@@ -138,22 +172,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'        => isset($row['tracker_id']) ? $row['tracker_id'] : null,
-                'title'            => $row[$title] ? $row[$title] : $row['title'],
-                'extendedProps'      => ['description' => $row[$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] : $row['coloring']) : ($this->getColor(isset($row[$coloring]) ? $row[$coloring] : '', $colormap)),
+                'color'            => ($coloring && isset($row[$coloring])) ? ($row[$coloring] ?: $row['coloring'] ?? '') : ($this->getColor($row[$coloring] ?? '', $colormap)),
                 'textColor'        => '#000',
-                'resourceId'       => ($resource && isset($row[$resource])) ? strtolower($row[$resource]) : '',
+                'resourceId'       => $resource && isset($row[$resource]) ? strtolower($row[$resource]) : '',
                 'resourceEditable' => true,
             ];
         }


=====================================
lib/core/Tracker/Field/DateTime.php
=====================================
@@ -353,4 +353,9 @@ class Tracker_Field_DateTime extends \Tracker\Field\AbstractItemField implements
                 })
         ];
     }
+
+    public function isDateOnlyCalendarValue(): bool
+    {
+        return $this->getOption('datetime') === 'd';
+    }
 }


=====================================
lib/core/Tracker/Field/JsCalendar.php
=====================================
@@ -96,13 +96,24 @@ 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();
 
-        if (! empty($value) && ! is_int((int) $value)) {    // prevent corrupted date values getting saved (e.g. from inline edit sometimes)
-            $value = '';
-            Feedback::error(tr('Date Picker Field: "%0" is not a valid internal date value', $value));
+        if (! empty($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;
+            }
+
+            if (! is_int((int) $value)) { // prevent corrupted date values getting saved (e.g. from inline edit sometimes)
+                $value = '';
+                Feedback::error(tr('Date Picker Field: "%0" is not a valid internal date value', $value));
+            }
         }
 
         return [



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

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