From c2164553d50d813ced520cab3000adfbce8a6835 Mon Sep 17 00:00:00 2001 From: Giorgio Date: Fri, 28 Aug 2026 09:52:14 +0200 Subject: [PATCH 1/2] fix(timeformat): make datetime-local inputs honor the chosen time format Native datetime-local inputs (Forgot-to-end-workday modals, workday history corrections, contact/deal/lead activity forms) rendered in the browser locale (12h AM/PM on en-US) regardless of the user's or system's time format preference. They are now initialized as Flatpickr datetime pickers (new user-datetime-input class) that display in the resolved date + time format while submitting the same YYYY-MM-DDTHH:MM wire value; native min/max bounds move to the picker. Also route the recurring-tasks last-run cell and the admin version-update published timestamp through formatUserDateTime instead of raw toLocaleString, and add regression tests (one enforces that every datetime-local input in the app stays prefs-aware). --- CHANGELOG.md | 4 ++ app/static/admin-version-update.js | 10 ++-- app/static/date-picker-init.js | 57 +++++++++++++++++++ .../contacts/communication_form.html | 4 +- app/templates/deals/activity_form.html | 4 +- app/templates/leads/activity_form.html | 4 +- app/templates/recurring_tasks/list.html | 4 +- .../workday/_auto_closed_clock_out_modal.html | 2 +- .../workday/_overnight_clock_out_modal.html | 2 +- app/templates/workday/history.html | 8 +-- tests/test_time_format_display.py | 42 ++++++++++++++ 11 files changed, 124 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5c24e0a..72e23e23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- **Datetime fields ignore the chosen time format** — Native `datetime-local` inputs (the "Forgot to end your workday?" workday modals, workday history corrections, contact/deal/lead activity forms) render in the browser locale — e.g. 12h AM/PM on en-US — no matter what time format the user or system settings chose. They are now initialized as Flatpickr datetime pickers that display in the user's preferred date + time format (24h by default) while still submitting the same wire format; min/max bounds move to the picker. A regression test enforces that every `datetime-local` input stays prefs-aware. The recurring-tasks "last run" cell and the admin version-update published timestamp also now honor the preference instead of the browser locale. + ## [5.13.2] - 2026-08-27 ### Added diff --git a/app/static/admin-version-update.js b/app/static/admin-version-update.js index a0653824..f278d039 100644 --- a/app/static/admin-version-update.js +++ b/app/static/admin-version-update.js @@ -87,10 +87,12 @@ if (data.published_at) { try { var d = new Date(data.published_at); - published.textContent = d.toLocaleString(undefined, { - dateStyle: "medium", - timeStyle: "short", - }); + published.textContent = window.formatUserDateTime + ? window.formatUserDateTime(d) + : d.toLocaleString(undefined, { + dateStyle: "medium", + timeStyle: "short", + }); } catch (e) { published.textContent = data.published_at; } diff --git a/app/static/date-picker-init.js b/app/static/date-picker-init.js index 24aefa95..b2b0d214 100644 --- a/app/static/date-picker-init.js +++ b/app/static/date-picker-init.js @@ -18,6 +18,19 @@ } } + function getDateTimeAltFormat() { + return getFlatpickrAltFormat() + ' ' + getTimeAltFormat(); + } + + /** + * Parse a wire datetime-local value ("YYYY-MM-DDTHH:MM") into a Date. + */ + function parseWireDateTime(value) { + if (!value) return undefined; + var d = new Date(value); + return isNaN(d.getTime()) ? undefined : d; + } + function getFirstDayOfWeek() { if (window.userPrefs && typeof window.userPrefs.weekStartDay === 'number' && window.userPrefs.weekStartDay >= 0 && window.userPrefs.weekStartDay <= 6) { return window.userPrefs.weekStartDay; @@ -194,9 +207,53 @@ }); } + /** + * Initialize Flatpickr on datetime-local inputs so they display using the + * user's preferred date + time formats while still submitting + * YYYY-MM-DDTHH:MM. Native datetime-local controls render in the browser + * locale (e.g. 12h AM/PM on en-US), ignoring the in-app preference. + */ + function initUserDateTimeInputs() { + if (typeof flatpickr === 'undefined') return; + var inputs = document.querySelectorAll('input.user-datetime-input[type="datetime-local"]'); + var altFormat = getDateTimeAltFormat(); + var use24hr = timePickerUses24hr(); + var firstDay = getFirstDayOfWeek(); + inputs.forEach(function (el) { + if (el._flatpickr) return; + // Preserve existing classes on the visible alt input (form-input, form-control, sizing). + var altClass = (el.className || 'form-input').replace(/\buser-datetime-input\b/g, '').trim() || 'form-input'; + var minDate = parseWireDateTime(el.getAttribute('min')); + var maxDate = parseWireDateTime(el.getAttribute('max')); + // Bounds move to Flatpickr; native min/max on the (now hidden) wire + // input would still block submission with browser validation. + el.removeAttribute('min'); + el.removeAttribute('max'); + flatpickr(el, { + enableTime: true, + dateFormat: 'Y-m-dTH:i', + time_24hr: use24hr, + altInput: true, + altFormat: altFormat, + altInputClass: altClass, + allowInput: false, + minDate: minDate, + maxDate: maxDate, + locale: { firstDayOfWeek: firstDay }, + // type=datetime-local fights Flatpickr; hide the native control. + onReady: function (_selectedDates, _dateStr, instance) { + if (instance.input) { + instance.input.style.display = 'none'; + } + } + }); + }); + } + function initAll() { initUserDateInputs(); initUserTimeInputs(); + initUserDateTimeInputs(); } // Test / debug hooks diff --git a/app/templates/contacts/communication_form.html b/app/templates/contacts/communication_form.html index 573b141a..9181e895 100644 --- a/app/templates/contacts/communication_form.html +++ b/app/templates/contacts/communication_form.html @@ -50,7 +50,7 @@
- +
@@ -65,7 +65,7 @@
- +
diff --git a/app/templates/deals/activity_form.html b/app/templates/deals/activity_form.html index 0a76e7a4..60f36aa1 100644 --- a/app/templates/deals/activity_form.html +++ b/app/templates/deals/activity_form.html @@ -40,11 +40,11 @@
- +
- +
diff --git a/app/templates/leads/activity_form.html b/app/templates/leads/activity_form.html index b08da96f..0d6b6e06 100644 --- a/app/templates/leads/activity_form.html +++ b/app/templates/leads/activity_form.html @@ -40,11 +40,11 @@
- +
- +
diff --git a/app/templates/recurring_tasks/list.html b/app/templates/recurring_tasks/list.html index e07b1bdc..02bda3cc 100644 --- a/app/templates/recurring_tasks/list.html +++ b/app/templates/recurring_tasks/list.html @@ -231,7 +231,9 @@ if (!res.ok) throw new Error(data.error || 'Run failed'); var lastCell = document.querySelector('.last-run-cell[data-task-id="' + taskId + '"]'); if (lastCell && data.last_created_at) { - lastCell.textContent = new Date(data.last_created_at).toLocaleString(); + lastCell.textContent = window.formatUserDateTime + ? window.formatUserDateTime(new Date(data.last_created_at)) + : new Date(data.last_created_at).toLocaleString( } if (window.toastManager?.success) { window.toastManager.success({{ _('Task created successfully')|tojson }}, '', 3000); diff --git a/app/templates/workday/_auto_closed_clock_out_modal.html b/app/templates/workday/_auto_closed_clock_out_modal.html index 76922490..61a3fe37 100644 --- a/app/templates/workday/_auto_closed_clock_out_modal.html +++ b/app/templates/workday/_auto_closed_clock_out_modal.html @@ -26,7 +26,7 @@

+ class="form-input w-full user-datetime-input">
- +
- +
@@ -145,8 +145,8 @@

{{ _('My correction requests') }}

- - + + diff --git a/tests/test_time_format_display.py b/tests/test_time_format_display.py index 26550d7e..5a3c7e0c 100644 --- a/tests/test_time_format_display.py +++ b/tests/test_time_format_display.py @@ -134,6 +134,48 @@ def test_date_picker_init_uses_24hr_from_prefs(): assert "formatDate: formatTimeDate" in src +def test_date_picker_init_covers_datetime_local_inputs(): + """datetime-local inputs must be flatpickr-initialized per user prefs. + + Native datetime-local controls render in the browser locale (12h AM/PM on + en-US) and ignore the in-app time format setting — regression test for the + "Forgot to end your workday?" modal showing a 12h leave time. + """ + src = Path("app/static/date-picker-init.js").read_text(encoding="utf-8") + assert "user-datetime-input" in src + assert "initUserDateTimeInputs" in src + # Wire format must stay datetime-local compatible (YYYY-MM-DDTHH:MM) + assert "'Y-m-dTH:i'" in src + # time_24hr must be honoured for the clock part + assert "time_24hr: use24hr" in src + # Native min/max bounds must move to Flatpickr, not stay on the hidden input + assert "removeAttribute('min')" in src + assert "removeAttribute('max')" in src + + +def test_workday_modals_use_user_datetime_input(): + """Both "Forgot to end your workday?" modals must use the prefs-aware picker.""" + for name in ( + "app/templates/workday/_auto_closed_clock_out_modal.html", + "app/templates/workday/_overnight_clock_out_modal.html", + ): + src = Path(name).read_text(encoding="utf-8") + assert "datetime-local" in src + assert "user-datetime-input" in src + + +def test_all_datetime_local_inputs_are_prefs_aware(): + """Every datetime-local input in the app must carry user-datetime-input.""" + tag_re = re.compile(r"]*>", re.S) + for path in Path("app/templates").rglob("*.html"): + src = path.read_text(encoding="utf-8", errors="ignore") + for tag in tag_re.findall(src): + if 'type="datetime-local"' in tag.replace("\n", " "): + assert "user-datetime-input" in tag.replace("\n", " "), ( + f"{path}: datetime-local input without user-datetime-input: {tag!r}" + ) + + def _run_parse_user_time_cases(): """Evaluate __parseUserTimeInput in Node with a minimal DOM stub.""" js_path = Path("app/static/date-picker-init.js").resolve() From be8c7a823d7b4dd120440591d2431f0768bf00c3 Mon Sep 17 00:00:00 2001 From: Giorgio Date: Fri, 28 Aug 2026 11:09:43 +0200 Subject: [PATCH 2/2] chore(ci): fix pre-existing flake8 W391 and missing script nonces on main The branch CI failed on two issues that already exist on main, unrelated to the time-format fix: a trailing blank line at the end of app/routes/reports.py (flake8 W391) and four inline -