From aee3476af11a06d4810deb72eef685b42ede4d34 Mon Sep 17 00:00:00 2001 From: ankurjuneja Date: Mon, 27 Jul 2026 13:40:07 -0700 Subject: [PATCH 1/5] Fix jquery-dateFormat parseTime dropping the time in colon-labelled timezones --- .../labkey/targetedms/view/instrumentCalendar.jsp | 5 +++-- .../tests/targetedms/InstrumentSchedulingTest.java | 3 +++ webapp/TargetedMS/js/scheduleUtils.js | 13 +++++++++++++ webapp/TargetedMS/js/scheduler.js | 7 +++++-- 4 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/org/labkey/targetedms/view/instrumentCalendar.jsp b/src/org/labkey/targetedms/view/instrumentCalendar.jsp index 2e6c35d1a..6fe5b5e73 100644 --- a/src/org/labkey/targetedms/view/instrumentCalendar.jsp +++ b/src/org/labkey/targetedms/view/instrumentCalendar.jsp @@ -23,6 +23,7 @@ { dependencies.add("internal/jQuery"); dependencies.add("targetedms/yearCalendar"); + dependencies.add("TargetedMS/js/scheduleUtils.js"); } %> @@ -51,8 +52,8 @@ $('#delete-event').css('display', event.annotation ? '' : 'none'); $('#event-modal input[name="event-description"]').val(event.annotation ? event.annotation.description : ''); - $('#event-modal input[name="event-start-date"]').val(startDate.getFullYear() + '-' + (startDate.getMonth() + 1 < 10 ? '0' : '') + (startDate.getMonth() + 1) + '-' + (startDate.getDate() < 10 ? '0' : '') + startDate.getDate()); - $('#event-modal input[name="event-end-date"]').val(endDate.getFullYear() + '-' + (endDate.getMonth() + 1 < 10 ? '0' : '') + (endDate.getMonth() + 1) + '-' + (endDate.getDate() < 10 ? '0' : '') + endDate.getDate()); + $('#event-modal input[name="event-start-date"]').val(ScheduleUtils.toDateValue(startDate)); + $('#event-modal input[name="event-end-date"]').val(ScheduleUtils.toDateValue(endDate)); $('#annotation-save-error').text(''); $('#event-modal').modal(); } diff --git a/test/src/org/labkey/test/tests/targetedms/InstrumentSchedulingTest.java b/test/src/org/labkey/test/tests/targetedms/InstrumentSchedulingTest.java index 1ce61bf63..3684a046f 100644 --- a/test/src/org/labkey/test/tests/targetedms/InstrumentSchedulingTest.java +++ b/test/src/org/labkey/test/tests/targetedms/InstrumentSchedulingTest.java @@ -183,6 +183,9 @@ public void testSchedule() throws IOException, CommandException { String originalStart = getFormElement(START_DATE_TIME_FIELD.findElement(getDriver())); String originalEnd = getFormElement(END_DATE_TIME_FIELD.findElement(getDriver())); + // Pin the 8AM/5PM defaults so a regression that zeroes or shifts the time is caught even on agents whose timezone would otherwise hide it. + assertTrue("Start field should default to 8AM, was: " + originalStart, originalStart.endsWith("T08:00")); + assertTrue("End field should default to 5PM, was: " + originalEnd, originalEnd.endsWith("T17:00")); // Try scheduling over the first reservation and verify it is blocked setFormElement(START_DATE_TIME_FIELD.findElement(getDriver()), originalStart.replace("-03T", "-02T")); setFormElement(END_DATE_TIME_FIELD.findElement(getDriver()), originalEnd.replace("-03T", "-02T")); diff --git a/webapp/TargetedMS/js/scheduleUtils.js b/webapp/TargetedMS/js/scheduleUtils.js index 02588526f..26846879b 100644 --- a/webapp/TargetedMS/js/scheduleUtils.js +++ b/webapp/TargetedMS/js/scheduleUtils.js @@ -8,6 +8,19 @@ (function(window) { const utils = {}; + const pad = function(n) { return (n < 10 ? '0' : '') + n; }; + + // Format a Date as datetime-local's fixed 'yyyy-MM-ddTHH:mm' wire form from local fields; avoids DateFormat, which zeroes the time in timezones whose label contains a colon (e.g. Honolulu). + utils.toDateTimeLocalValue = function(date) { + return date.getFullYear() + '-' + pad(date.getMonth() + 1) + '-' + pad(date.getDate()) + + 'T' + pad(date.getHours()) + ':' + pad(date.getMinutes()); + }; + + // Date-only 'yyyy-MM-dd' wire form, derived from the datetime variant. + utils.toDateValue = function(date) { + return utils.toDateTimeLocalValue(date).slice(0, 10); + }; + // Convert a CSS color string (named, rgb, hex) to standard 6-digit HEX color (#RRGGBB) utils.stringToColor = function(color) { if (!color) return '#888888'; diff --git a/webapp/TargetedMS/js/scheduler.js b/webapp/TargetedMS/js/scheduler.js index f289542e2..c9449f989 100644 --- a/webapp/TargetedMS/js/scheduler.js +++ b/webapp/TargetedMS/js/scheduler.js @@ -396,8 +396,8 @@ $(function() { endDate.setDate(endDate.getDate() - 1); } - let startDateFormatted = DateFormat.format.date(startDate, LABKEY.container.formats.dateTimeFormat); - let endDateFormatted = DateFormat.format.date(endDate, LABKEY.container.formats.dateTimeFormat); + let startDateFormatted = ScheduleUtils.toDateTimeLocalValue(startDate); + let endDateFormatted = ScheduleUtils.toDateTimeLocalValue(endDate); // remove the old event log rows removeEventLog(); @@ -581,6 +581,9 @@ $(function() { }); function fetchInstrumentCosts(instrumentId, startDate, endDate) { + // Normalize to Date: callers pass either Date objects (hover) or seconds-less datetime-local strings (post-save), which DateFormat can't parse as-is. + startDate = new Date(startDate); + endDate = new Date(endDate); LABKEY.Query.selectRows({ schemaName: 'targetedms', queryName: 'instrumentRate', From b92cfdccc10daf63e0ef675bdbbedd78b5360b6c Mon Sep 17 00:00:00 2001 From: ankurjuneja Date: Mon, 27 Jul 2026 14:14:39 -0700 Subject: [PATCH 2/5] claude cr findings and fixes --- .../targetedms/InstrumentSchedulingTest.java | 3 +++ webapp/TargetedMS/js/scheduleUtils.js | 13 ++++++------- webapp/TargetedMS/js/scheduler.js | 19 ++++++++++--------- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/test/src/org/labkey/test/tests/targetedms/InstrumentSchedulingTest.java b/test/src/org/labkey/test/tests/targetedms/InstrumentSchedulingTest.java index 3684a046f..ea9905ba9 100644 --- a/test/src/org/labkey/test/tests/targetedms/InstrumentSchedulingTest.java +++ b/test/src/org/labkey/test/tests/targetedms/InstrumentSchedulingTest.java @@ -200,6 +200,9 @@ public void testSchedule() throws IOException, CommandException assertProjectEventCounts(2, 0); + // The event chip time range is rendered via DateFormat (the patched parseTime path); verify it shows the real 8AM-5PM range, not a zeroed 00:00 - 00:00 as happened in colon-labelled timezones. + assertEquals("Event chip should show the 8AM-5PM time range", "08:00 - 17:00", getText(Locator.css(".activeProjectEvent .event-date"))); + doAndWaitForPageToLoad(() -> selectOptionByText(PROJECT_DROP_DOWN, PROJECT_2)); scheduleInstrument(yearMonth + "-04", false); diff --git a/webapp/TargetedMS/js/scheduleUtils.js b/webapp/TargetedMS/js/scheduleUtils.js index 26846879b..7b4199c36 100644 --- a/webapp/TargetedMS/js/scheduleUtils.js +++ b/webapp/TargetedMS/js/scheduleUtils.js @@ -10,15 +10,14 @@ const pad = function(n) { return (n < 10 ? '0' : '') + n; }; - // Format a Date as datetime-local's fixed 'yyyy-MM-ddTHH:mm' wire form from local fields; avoids DateFormat, which zeroes the time in timezones whose label contains a colon (e.g. Honolulu). - utils.toDateTimeLocalValue = function(date) { - return date.getFullYear() + '-' + pad(date.getMonth() + 1) + '-' + pad(date.getDate()) - + 'T' + pad(date.getHours()) + ':' + pad(date.getMinutes()); + // Date-only 'yyyy-MM-dd' wire form from local fields. + utils.toDateValue = function(date) { + return date.getFullYear() + '-' + pad(date.getMonth() + 1) + '-' + pad(date.getDate()); }; - // Date-only 'yyyy-MM-dd' wire form, derived from the datetime variant. - utils.toDateValue = function(date) { - return utils.toDateTimeLocalValue(date).slice(0, 10); + // Format a Date as datetime-local's fixed 'yyyy-MM-ddTHH:mm' wire form from local fields; avoids DateFormat, which zeroes the time in timezones whose label contains a colon (e.g. Honolulu). + utils.toDateTimeLocalValue = function(date) { + return utils.toDateValue(date) + 'T' + pad(date.getHours()) + ':' + pad(date.getMinutes()); }; // Convert a CSS color string (named, rgb, hex) to standard 6-digit HEX color (#RRGGBB) diff --git a/webapp/TargetedMS/js/scheduler.js b/webapp/TargetedMS/js/scheduler.js index c9449f989..622221018 100644 --- a/webapp/TargetedMS/js/scheduler.js +++ b/webapp/TargetedMS/js/scheduler.js @@ -88,10 +88,7 @@ $(function() { let bgColor = e.event.extendedProps.project === project ? e.backgroundColor : 'gray'; const cl = e.event.extendedProps.project === project ? 'activeProjectEvent' : 'otherProjectEvent'; let textColor = ScheduleUtils.getContrastTextColor(ScheduleUtils.stringToColor(bgColor)); - let timeFormatString = LABKEY.container.formats.timeFormat; - // Strip seconds and milliseconds - timeFormatString = timeFormatString.replace(':ss', '').replace('.SSS', ''); - let dateStr = DateFormat.format.date(e.event.start, timeFormatString) + ' - ' + DateFormat.format.date(e.event.end, timeFormatString); + let dateStr = ScheduleUtils.formatTimeRange(e.event.start, e.event.end); let style = 'background-color: ' + LABKEY.Utils.encodeHtml(bgColor) + '; color: ' + LABKEY.Utils.encodeHtml(textColor) + ';' + 'width: 100%'; content += '
' + '
' + LABKEY.Utils.encodeHtml(dateStr) + '
' @@ -582,8 +579,12 @@ $(function() { function fetchInstrumentCosts(instrumentId, startDate, endDate) { // Normalize to Date: callers pass either Date objects (hover) or seconds-less datetime-local strings (post-save), which DateFormat can't parse as-is. - startDate = new Date(startDate); - endDate = new Date(endDate); + const start = new Date(startDate); + const end = new Date(endDate); + // Bail on degenerate input so the cost log never renders $NaN or garbage dates (mirrors calculateAndRenderCostPreview). + if (isNaN(start.getTime()) || isNaN(end.getTime()) || end <= start) { + return; + } LABKEY.Query.selectRows({ schemaName: 'targetedms', queryName: 'instrumentRate', @@ -598,7 +599,7 @@ $(function() { } let fee = data.rows[0].fee; let rateType = data.rows[0].rateType; - let cost = fee * (Math.abs(new Date(endDate) - new Date(startDate))) / 1000 / 60 / 60; + let cost = fee * (Math.abs(end - start)) / 1000 / 60 / 60; // query the rateType LABKEY.Query.selectRows({ @@ -611,8 +612,8 @@ $(function() { success: function (data) { let setupFee = data.rows[0].setupFee; - let startDateFormatted = DateFormat.format.date(startDate, LABKEY.container.formats.dateTimeFormat); - let endDateFormatted = DateFormat.format.date(endDate, LABKEY.container.formats.dateTimeFormat); + let startDateFormatted = DateFormat.format.date(start, LABKEY.container.formats.dateTimeFormat); + let endDateFormatted = DateFormat.format.date(end, LABKEY.container.formats.dateTimeFormat); let tableElt = document.getElementById('event-cost-table'); let rowElt = document.createElement('tr'); From c94ad2cb827ea591bed5166149a13af0b6ad0b06 Mon Sep 17 00:00:00 2001 From: ankurjuneja Date: Tue, 28 Jul 2026 17:01:27 -0700 Subject: [PATCH 3/5] use shared formatTimeRange in the all instruments view --- resources/views/scheduleAllInstruments.html | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/resources/views/scheduleAllInstruments.html b/resources/views/scheduleAllInstruments.html index 68b8af10e..98948636a 100644 --- a/resources/views/scheduleAllInstruments.html +++ b/resources/views/scheduleAllInstruments.html @@ -42,10 +42,7 @@ selectMirror: false, eventContent: function(arg) { const e = arg.event; - const timeFormatString = LABKEY.container.formats.timeFormat - .replace(':ss', '') - .replace('.SSS', ''); - const dateStr = DateFormat.format.date(e.start, timeFormatString) + ' - ' + DateFormat.format.date(e.end, timeFormatString); + const dateStr = ScheduleUtils.formatTimeRange(e.start, e.end); const baseColor = e.extendedProps && e.extendedProps.baseColor ? e.extendedProps.baseColor : (arg.backgroundColor || e.backgroundColor || '#888'); const bg = (selectedProjectId && e.extendedProps && e.extendedProps.projectId !== selectedProjectId) ? 'gray' : baseColor; const textColor = ScheduleUtils.getContrastTextColor(ScheduleUtils.stringToColor(bg)); From 861ed66e283e125c4730930eb45e91900bd5094d Mon Sep 17 00:00:00 2001 From: ankurjuneja Date: Tue, 28 Jul 2026 17:11:22 -0700 Subject: [PATCH 4/5] route cost lookup failure to the real error element, drop dead cost selectors --- webapp/TargetedMS/js/scheduler.js | 22 +++------------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/webapp/TargetedMS/js/scheduler.js b/webapp/TargetedMS/js/scheduler.js index 622221018..6abfa3358 100644 --- a/webapp/TargetedMS/js/scheduler.js +++ b/webapp/TargetedMS/js/scheduler.js @@ -407,7 +407,6 @@ $(function() { $('#delete-event').toggle(!!event.id); $('#add-event').text('Save'); $('#schedule-save-error').text(''); - $('#schedule-cost-error').text(''); $('#event-modal').modal(); } @@ -516,7 +515,7 @@ $(function() { } let fee = data.rows[0].fee; let rateType = data.rows[0].rateType; - let cost = fee * (Math.abs(end - start)) / 1000 / 60 / 60; + let cost = fee * (end - start) / 1000 / 60 / 60; // the guard above establishes end > start LABKEY.Query.selectRows({ schemaName: 'targetedms', @@ -599,7 +598,7 @@ $(function() { } let fee = data.rows[0].fee; let rateType = data.rows[0].rateType; - let cost = fee * (Math.abs(end - start)) / 1000 / 60 / 60; + let cost = fee * (end - start) / 1000 / 60 / 60; // the guard above establishes end > start // query the rateType LABKEY.Query.selectRows({ @@ -619,8 +618,6 @@ $(function() { let rowElt = document.createElement('tr'); rowElt.className = 'labkey-row'; let totalCost = Math.round(((setupFee + cost) + Number.EPSILON) * 100) / 100; - cost = Math.round((cost + Number.EPSILON) * 100) / 100; - setupFee = Math.round((setupFee + Number.EPSILON) * 100) / 100; rowElt.innerHTML = '' + startDateFormatted + '' + endDateFormatted + '' + '$' + totalCost.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 @@ -632,22 +629,9 @@ $(function() { minimumFractionDigits: 2, maximumFractionDigits: 2 }); - - $('#setup-cost').val('$' + setupFee.toLocaleString('en-US', { - minimumFractionDigits: 2, - maximumFractionDigits: 2 - })); - $('#instrument-fee').val('$' + cost.toLocaleString('en-US', { - minimumFractionDigits: 2, - maximumFractionDigits: 2 - })); - $('#total-cost').val('$' + (setupFee + cost).toLocaleString('en-US', { - minimumFractionDigits: 2, - maximumFractionDigits: 2 - })); }, failure: function (errorInfo) { - $('#schedule-cost-error').text('Error calculating cost ' + (errorInfo.exception ? errorInfo.exception : '')); + $('#schedule-save-error').text('Error calculating cost ' + (errorInfo.exception ? errorInfo.exception : '')); } }); } From 17a53af67f8fbfe95982495114c1614158492fd5 Mon Sep 17 00:00:00 2001 From: ankurjuneja Date: Tue, 28 Jul 2026 17:13:42 -0700 Subject: [PATCH 5/5] guard empty rateType results so the lookup reports instead of throwing --- webapp/TargetedMS/js/scheduler.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/webapp/TargetedMS/js/scheduler.js b/webapp/TargetedMS/js/scheduler.js index 6abfa3358..3c790c76b 100644 --- a/webapp/TargetedMS/js/scheduler.js +++ b/webapp/TargetedMS/js/scheduler.js @@ -525,6 +525,10 @@ $(function() { LABKEY.Filter.create('Id', rateType, LABKEY.Filter.Types.EQUAL) ], success: function (rt) { + if (rt.rows.length === 0) { + previewErrorEl.text('No rate type found for instrument.'); + return; + } let setupFee = rt.rows[0].setupFee; let instrumentFee = Math.round((cost + Number.EPSILON) * 100) / 100; setupFee = Math.round((setupFee + Number.EPSILON) * 100) / 100; @@ -609,6 +613,10 @@ $(function() { LABKEY.Filter.create('Id', rateType, LABKEY.Filter.Types.EQUAL) ], success: function (data) { + if (data.rows.length === 0) { + $('#schedule-save-error').text('Error calculating cost. No rate type found for instrument'); + return; + } let setupFee = data.rows[0].setupFee; let startDateFormatted = DateFormat.format.date(start, LABKEY.container.formats.dateTimeFormat);