From a0d4f9d3b995757947d62bc01d26ad3a807331d2 Mon Sep 17 00:00:00 2001 From: Oleh Maikovych Date: Wed, 27 May 2026 15:49:47 +0300 Subject: [PATCH 01/27] #2750 Added locale to fullcalendar configuration to properly format dates on calendar. --- .../platform/web/components/fullcalendar/tg-fullcalendar.js | 1 + 1 file changed, 1 insertion(+) diff --git a/platform-web-ui/src/main/web/ua/com/fielden/platform/web/components/fullcalendar/tg-fullcalendar.js b/platform-web-ui/src/main/web/ua/com/fielden/platform/web/components/fullcalendar/tg-fullcalendar.js index 64aacc6687a..14b34b84afb 100644 --- a/platform-web-ui/src/main/web/ua/com/fielden/platform/web/components/fullcalendar/tg-fullcalendar.js +++ b/platform-web-ui/src/main/web/ua/com/fielden/platform/web/components/fullcalendar/tg-fullcalendar.js @@ -191,6 +191,7 @@ export class TgFullcalendar extends mixinBehaviors([IronResizableBehavior], Poly const config = { initialView: 'dayGridMonth', headerToolbar: false, + locale: this._appConfig.locale, datesSet: (dataInfo) => { this.calendarTitle = dataInfo.view.title; }, From 231331e1706ea6b62933a3bdb898cc37b88e3b67 Mon Sep 17 00:00:00 2001 From: Oleh Maikovych Date: Wed, 27 May 2026 15:51:35 +0300 Subject: [PATCH 02/27] #2750 Implemented entity splitting into parts to add long lasting events to allday cell on top of the fullcalendar. Also implemented logic that rebuilds dataset for fullcalendar when switching between daygrid view to time grid view. --- .../fullcalendar/tg-fullcalendar.js | 82 +++++++++++++++++-- 1 file changed, 74 insertions(+), 8 deletions(-) diff --git a/platform-web-ui/src/main/web/ua/com/fielden/platform/web/components/fullcalendar/tg-fullcalendar.js b/platform-web-ui/src/main/web/ua/com/fielden/platform/web/components/fullcalendar/tg-fullcalendar.js index 14b34b84afb..eac4fb62bc3 100644 --- a/platform-web-ui/src/main/web/ua/com/fielden/platform/web/components/fullcalendar/tg-fullcalendar.js +++ b/platform-web-ui/src/main/web/ua/com/fielden/platform/web/components/fullcalendar/tg-fullcalendar.js @@ -287,11 +287,23 @@ export class TgFullcalendar extends mixinBehaviors([IronResizableBehavior], Poly _changeView(e) { if (this._calendar) { - this._calendar.changeView(this.viewTypes[e.detail].valueToSet); + const newView = this.viewTypes[e.detail].valueToSet; + this._calendar.changeView(newView); + this.currentView = newView; } tearDownEvent(e); } + _currentViewChanged(newView, oldView) { + if (oldView !== undefined && this._calendar) { + const wasMonth = oldView === 'dayGridMonth'; + const isMonth = newView === 'dayGridMonth'; + if (wasMonth !== isMonth) { + this._updateEventSource(this.entities, this.eventKeyProperty, this.eventDescProperty, this.eventFromProperty, this.eventToProperty, this._calendar); + } + } + } + _showLegend() { this.$.dropdown.open(); } @@ -300,23 +312,77 @@ export class TgFullcalendar extends mixinBehaviors([IronResizableBehavior], Poly * Updates calendar data; moves it to the date of the chronologically first event (if any); re-renders calendar. */ _updateEventSource(entities, eventKeyProperty, eventDescProperty, eventFromProperty, eventToProperty, _calendar) { + // Sentinels for open-ended events. Far enough outside any plausible calendar view to behave like ±infinity. + const FAR_PAST = moment('1900-01-01').toDate(); + const FAR_FUTURE = moment('2200-01-01').toDate(); if (allDefined(entities, eventKeyProperty, eventDescProperty, eventFromProperty, eventToProperty) && _calendar) { _calendar.getEvents().forEach(event => event.remove()); let startTime = Infinity; + // Month view shows a single continuous bar per entity; time-grid views split into timed + allDay segments. + const isMonth = this.currentView === 'dayGridMonth'; entities.forEach(entity => { if (startTime > entity.get(eventFromProperty)) { startTime = entity.get(eventFromProperty); } const eventColor = this.colorProperty ? entity.get(this.colorProperty) : undefined; - _calendar.addEvent({ - extendedProps: { - entity: entity - }, + const startVal = entity.get(eventFromProperty); + const endVal = entity.get(eventToProperty); + const common = { + extendedProps: { entity: entity }, title: entity.get(eventKeyProperty) + (eventDescProperty && entity.get(eventDescProperty) ? " - "+ entity.get(eventDescProperty) : ""), - start: entity.get(eventFromProperty), - end: entity.get(eventToProperty), backgroundColor: eventColor ? '#' + eventColor["hashlessUppercasedColourValue"] : "#3788d8", - }); + groupId: 'evt-' + entity.get('id') + }; + if (isMonth) { + if (!startVal && !endVal) { + _calendar.addEvent({ ...common, start: FAR_PAST, end: FAR_FUTURE, allDay: true }); + } else if (!startVal) { + _calendar.addEvent({ ...common, start: FAR_PAST, end: moment(endVal).toDate(), allDay: true }); + } else if (!endVal) { + _calendar.addEvent({ ...common, start: moment(startVal).toDate(), end: FAR_FUTURE, allDay: true }); + } else { + _calendar.addEvent({ ...common, start: startVal, end: endVal }); + } + } else if (!startVal && !endVal) { + _calendar.addEvent({ ...common, start: FAR_PAST, end: FAR_FUTURE, allDay: true }); + } else if (!startVal) { + const end = moment(endVal); + const endDayStart = end.clone().startOf('day'); + _calendar.addEvent({ ...common, start: FAR_PAST, end: endDayStart.toDate(), allDay: true }); + if (endDayStart.isBefore(end)) { + _calendar.addEvent({ ...common, start: endDayStart.toDate(), end: end.toDate() }); + } + } else if (!endVal) { + const start = moment(startVal); + const allDayStart = start.clone().startOf('day'); + if (allDayStart.isBefore(start)) { + allDayStart.add(1, 'day'); + } + if (start.isBefore(allDayStart)) { + _calendar.addEvent({ ...common, start: start.toDate(), end: allDayStart.toDate() }); + } + _calendar.addEvent({ ...common, start: allDayStart.toDate(), end: FAR_FUTURE, allDay: true }); + } else { + const start = moment(startVal); + const end = moment(endVal); + const fullDayStart = start.clone().startOf('day'); + if (fullDayStart.isBefore(start)) { + fullDayStart.add(1, 'day'); + } + const fullDayEnd = end.clone().startOf('day'); + if (!fullDayStart.isBefore(fullDayEnd)) { + _calendar.addEvent({ ...common, start: startVal, end: endVal }); + } else { + if (start.isBefore(fullDayStart)) { + _calendar.addEvent({ ...common, start: start.toDate(), end: fullDayStart.toDate() }); + } + // allDay events use exclusive end semantics in FullCalendar. + _calendar.addEvent({ ...common, start: fullDayStart.toDate(), end: fullDayEnd.toDate(), allDay: true }); + if (fullDayEnd.isBefore(end)) { + _calendar.addEvent({ ...common, start: fullDayEnd.toDate(), end: end.toDate() }); + } + } + } if (this.dataChangeReason !== RunActions.refresh && startTime && startTime < Infinity) { _calendar.gotoDate(startTime); } From 342e19ddf7852e707d6db4860696bf689ecb9a44 Mon Sep 17 00:00:00 2001 From: Oleh Maikovych Date: Wed, 27 May 2026 16:16:42 +0300 Subject: [PATCH 03/27] #2750 Minor code cleanup. --- .../platform/web/components/fullcalendar/tg-fullcalendar.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/platform-web-ui/src/main/web/ua/com/fielden/platform/web/components/fullcalendar/tg-fullcalendar.js b/platform-web-ui/src/main/web/ua/com/fielden/platform/web/components/fullcalendar/tg-fullcalendar.js index eac4fb62bc3..b71d37a8064 100644 --- a/platform-web-ui/src/main/web/ua/com/fielden/platform/web/components/fullcalendar/tg-fullcalendar.js +++ b/platform-web-ui/src/main/web/ua/com/fielden/platform/web/components/fullcalendar/tg-fullcalendar.js @@ -337,9 +337,9 @@ export class TgFullcalendar extends mixinBehaviors([IronResizableBehavior], Poly if (!startVal && !endVal) { _calendar.addEvent({ ...common, start: FAR_PAST, end: FAR_FUTURE, allDay: true }); } else if (!startVal) { - _calendar.addEvent({ ...common, start: FAR_PAST, end: moment(endVal).toDate(), allDay: true }); + _calendar.addEvent({ ...common, start: FAR_PAST, end: endVal, allDay: true }); } else if (!endVal) { - _calendar.addEvent({ ...common, start: moment(startVal).toDate(), end: FAR_FUTURE, allDay: true }); + _calendar.addEvent({ ...common, start: startVal, end: FAR_FUTURE, allDay: true }); } else { _calendar.addEvent({ ...common, start: startVal, end: endVal }); } From d9ad20cf70a173d1b1b0ad03f7ff2f5f4803f3d1 Mon Sep 17 00:00:00 2001 From: jhou-pro Date: Sun, 31 May 2026 10:57:05 +0300 Subject: [PATCH 04/27] #2755 Trim '@DateOnly' property time portion for 'n' approximation. This applies to all kinds of properties: 1. Fixed time-zone properties (only UTC). 2. Simple properties of whatever kinds (including @DependentTimeZoneMode ones). --- .../platform/web/reflection/tg-date-utils.js | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/platform-web-ui/src/main/web/ua/com/fielden/platform/web/reflection/tg-date-utils.js b/platform-web-ui/src/main/web/ua/com/fielden/platform/web/reflection/tg-date-utils.js index 9c99c95a51f..56d26f22131 100644 --- a/platform-web-ui/src/main/web/ua/com/fielden/platform/web/reflection/tg-date-utils.js +++ b/platform-web-ui/src/main/web/ua/com/fielden/platform/web/reflection/tg-date-utils.js @@ -114,6 +114,19 @@ export function _timeZoneHeader () { return {"Time-Zone": moment.tz.guess(true)}; }; +/** + * Trims Moment JS 'dateTime' instance's time portion in case where `prop.isDate()` is true. + * Does nothing otherwise. + * + * @param prop {Object, likely EntityTypeProp} -- optional meta-property object, optionally containing `isDate`. + */ +function _trimDateOnlyTimePortion(dateTime, prop) { + if (dateTime && prop?.isDate?.()) { + return dateTime.startOf('day'); + } + return dateTime; +}; + /** * Returns time-zone mode-specific 'now' moment. * @@ -122,7 +135,7 @@ export function _timeZoneHeader () { export function now(prop) { // In concrete time-zone (e.g. UTC) just use standard method _momentTz for creating 'now' in that time-zone. if (_fixedTimeZone(prop)) { - return _momentTz(prop); + return _trimDateOnlyTimePortion(_momentTz(prop), prop); } // In independent time-zone mode we do the following trick: // 1. create 'now' moment in current surrogate (equal to server one) time-zone; @@ -131,5 +144,11 @@ export function now(prop) { // 4. then use that string to create moment in surrogate time-zone. // In dependent time-zone mode this trick will return the same moment object as just moment(). // Take into account @DependentTimeZoneMode properties. - return _enforceDependentTimeZoneModeFor(() => moment(moment().tz(moment.tz.guess(true)).format('YYYY-MM-DD HH:mm:ss.SSS')), prop); + return _trimDateOnlyTimePortion( + _enforceDependentTimeZoneModeFor( + () => moment(moment().tz(moment.tz.guess(true)).format('YYYY-MM-DD HH:mm:ss.SSS')), + prop + ), + prop + ); }; \ No newline at end of file From 61291e32112d3f7d28479a2f65f0c901d2f598bb Mon Sep 17 00:00:00 2001 From: jhou-pro Date: Mon, 1 Jun 2026 10:56:05 +0300 Subject: [PATCH 05/27] #2755 Resolve the current moment via `now(prop)` instead of `_momentTz(prop)` in date-picker components. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The four call sites that build a fresh "now" moment — the default date in `tg-calendar`, the highlighted today and the fallback month in `tg-month-selector`, and the auto-inserted current year in tg-datetime-picker` — now use `now(prop)`. Unlike `_momentTz(prop)`, which returns the current instant in the surrogate (server) time-zone, `now(prop)` re-resolves it in the user's real time-zone under independent time-zone mode, and applies `@DateOnly` trimming and the `@DependentTimeZoneMode` override consistently, so these defaults stay correct across time-zone and date boundaries. The `_momentTz` calls that convert an explicit value (a selected date, year/month/day objects, parsed strings or an entity value) are intentionally left unchanged, since they are not "now" creators. --- .../platform/web/components/tg-calendar.js | 20 +++++++++------ .../web/components/tg-month-selector.js | 25 +++++++++++-------- .../web/editors/tg-datetime-picker.js | 17 +++++++------ 3 files changed, 37 insertions(+), 25 deletions(-) diff --git a/platform-web-ui/src/main/web/ua/com/fielden/platform/web/components/tg-calendar.js b/platform-web-ui/src/main/web/ua/com/fielden/platform/web/components/tg-calendar.js index 06a74b1317d..c7dacd90ecf 100644 --- a/platform-web-ui/src/main/web/ua/com/fielden/platform/web/components/tg-calendar.js +++ b/platform-web-ui/src/main/web/ua/com/fielden/platform/web/components/tg-calendar.js @@ -14,7 +14,7 @@ import {Polymer} from '/resources/polymer/@polymer/polymer/lib/legacy/polymer-fn import {html} from '/resources/polymer/@polymer/polymer/lib/utils/html-tag.js'; import moment from '/resources/polymer/lib/moment-lib.js'; // used for moment.localeData() and moment.monthsShort(). -import { _momentTz } from '/resources/reflection/tg-date-utils.js'; +import { _momentTz, now } from '/resources/reflection/tg-date-utils.js'; const template = html`