From 787575807d2899a98a9657c4beb9817e47576aa0 Mon Sep 17 00:00:00 2001 From: AdrianoDev Date: Tue, 6 Oct 2026 11:39:26 +0200 Subject: [PATCH] frontend: date UTC convertite in ora italiana alla presentazione (#42) Co-Authored-By: Claude Sonnet 5.5 --- src/frontend/flask_app/app.py | 23 +++++++++++++- src/frontend/flask_app/config.py | 2 ++ .../flask_app/static/js/csv-export.js | 2 +- .../templates/measure/task_complete.html | 2 +- .../templates/statistics/dashboard.html | 8 ++--- src/frontend/flask_app/tests/test_local_dt.py | 31 +++++++++++++++++++ src/frontend/flask_app/tests/test_measure.py | 15 +++++++++ 7 files changed, 76 insertions(+), 7 deletions(-) create mode 100644 src/frontend/flask_app/tests/test_local_dt.py diff --git a/src/frontend/flask_app/app.py b/src/frontend/flask_app/app.py index 7dfe09d..d1942be 100644 --- a/src/frontend/flask_app/app.py +++ b/src/frontend/flask_app/app.py @@ -2,7 +2,8 @@ import json import os import re -from datetime import date +from datetime import date, datetime, timezone +from zoneinfo import ZoneInfo from urllib.parse import urlparse from flask import Flask, flash, g, jsonify, redirect, url_for, session, request @@ -154,6 +155,26 @@ def create_app() -> Flask: bolded = _BOLD_RX.sub(r"\1", escaped) return Markup(bolded.replace("\n", "
")) + @app.template_filter("local_dt") + def local_dt_filter(value, fmt="%d/%m/%Y %H:%M"): + """Show a UTC datetime (ISO string or datetime) in DISPLAY_TIMEZONE. + + A value without timezone is taken as UTC. None or empty gives "-". + """ + if value is None or value == "": + return "-" + if isinstance(value, str): + try: + value = datetime.fromisoformat(value.strip().replace("Z", "+00:00")) + except ValueError: + return value + if not isinstance(value, datetime): + return str(value) + if value.tzinfo is None: + value = value.replace(tzinfo=timezone.utc) + tz = ZoneInfo(app.config.get("DISPLAY_TIMEZONE") or "Europe/Rome") + return value.astimezone(tz).strftime(fmt) + @app.template_filter("tojson_attr") def tojson_attr_filter(value): """JSON encode safe for HTML attributes (x-data, etc.). diff --git a/src/frontend/flask_app/config.py b/src/frontend/flask_app/config.py index c1528a4..6533c08 100644 --- a/src/frontend/flask_app/config.py +++ b/src/frontend/flask_app/config.py @@ -56,6 +56,8 @@ class Config: # Babel i18n BABEL_DEFAULT_LOCALE = "it" BABEL_DEFAULT_TIMEZONE = "Europe/Rome" + # Il DB e le API parlano UTC; si converte solo alla presentazione. + DISPLAY_TIMEZONE: str = os.getenv("DISPLAY_TIMEZONE", "Europe/Rome") LANGUAGES = {"it": "Italiano", "en": "English"} diff --git a/src/frontend/flask_app/static/js/csv-export.js b/src/frontend/flask_app/static/js/csv-export.js index d618707..bd5c685 100644 --- a/src/frontend/flask_app/static/js/csv-export.js +++ b/src/frontend/flask_app/static/js/csv-export.js @@ -271,7 +271,7 @@ function csvExport() { */ generateFilename(prefix) { const now = new Date(); - const dateStr = now.toISOString().slice(0, 10); // YYYY-MM-DD + const dateStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`; // YYYY-MM-DD locale const timeStr = now.toTimeString().slice(0, 5).replace(':', ''); // HHMM return `${prefix}_${dateStr}_${timeStr}.csv`; diff --git a/src/frontend/flask_app/templates/measure/task_complete.html b/src/frontend/flask_app/templates/measure/task_complete.html index bb4da8c..daf69b2 100644 --- a/src/frontend/flask_app/templates/measure/task_complete.html +++ b/src/frontend/flask_app/templates/measure/task_complete.html @@ -183,7 +183,7 @@ {% for m in measurements|sort(attribute='subtask.marker_number')|sort(attribute='task_info.order_index') %} - {{ m.measured_at[:16]|replace('T', ' ') if m.measured_at else '-' }} + {{ m.measured_at|local_dt }} {{ m.task_info.title if m.task_info else '-' }} diff --git a/src/frontend/flask_app/templates/statistics/dashboard.html b/src/frontend/flask_app/templates/statistics/dashboard.html index 888d919..5945cad 100644 --- a/src/frontend/flask_app/templates/statistics/dashboard.html +++ b/src/frontend/flask_app/templates/statistics/dashboard.html @@ -312,8 +312,8 @@ function spcDashboard() { const params = new URLSearchParams(); params.set('recipe_id', this.selectedRecipeId); if (this.selectedSubtaskId) params.set('subtask_id', this.selectedSubtaskId); - if (this.dateFrom) params.set('date_from', this.dateFrom + 'T00:00:00'); - if (this.dateTo) params.set('date_to', this.dateTo + 'T23:59:59'); + if (this.dateFrom) params.set('date_from', new Date(this.dateFrom + 'T00:00:00').toISOString()); + if (this.dateTo) params.set('date_to', new Date(this.dateTo + 'T23:59:59.999').toISOString()); try { // Parallel fetch of all endpoints @@ -417,8 +417,8 @@ function spcDashboard() { const params = new URLSearchParams(); params.set('recipe_id', this.selectedRecipeId); if (this.selectedSubtaskId) params.set('subtask_id', this.selectedSubtaskId); - if (this.dateFrom) params.set('date_from', this.dateFrom + 'T00:00:00'); - if (this.dateTo) params.set('date_to', this.dateTo + 'T23:59:59'); + if (this.dateFrom) params.set('date_from', new Date(this.dateFrom + 'T00:00:00').toISOString()); + if (this.dateTo) params.set('date_to', new Date(this.dateTo + 'T23:59:59.999').toISOString()); const url = '/statistics/api/report-' + type + '?' + params.toString(); const resp = await fetch(url); diff --git a/src/frontend/flask_app/tests/test_local_dt.py b/src/frontend/flask_app/tests/test_local_dt.py new file mode 100644 index 0000000..a9f315b --- /dev/null +++ b/src/frontend/flask_app/tests/test_local_dt.py @@ -0,0 +1,31 @@ +"""Filtro local_dt: il backend parla UTC, la presentazione e' in Europe/Rome (#42).""" +from datetime import datetime, timezone + +import pytest + + +@pytest.fixture +def local_dt(flask_app): + return flask_app.jinja_env.filters["local_dt"] + + +def test_summer_time(local_dt): + assert local_dt("2026-10-06T08:00:00+00:00") == "06/10/2026 10:00" + + +def test_winter_time(local_dt): + assert local_dt("2026-01-15T08:00:00+00:00") == "15/01/2026 09:00" + + +def test_naive_is_utc(local_dt): + assert local_dt("2026-10-06T08:00:00") == "06/10/2026 10:00" + + +def test_datetime_object_and_format(local_dt): + dt = datetime(2026, 10, 6, 8, 0, tzinfo=timezone.utc) + assert local_dt(dt, "%H:%M") == "10:00" + + +@pytest.mark.parametrize("empty", [None, ""]) +def test_empty(local_dt, empty): + assert local_dt(empty) == "-" diff --git a/src/frontend/flask_app/tests/test_measure.py b/src/frontend/flask_app/tests/test_measure.py index 8188050..69379a1 100644 --- a/src/frontend/flask_app/tests/test_measure.py +++ b/src/frontend/flask_app/tests/test_measure.py @@ -407,6 +407,21 @@ class TestTaskComplete: assert resp.status_code == 200 assert b"productionClock(" in resp.data + def test_measured_at_shown_in_local_time(self, logged_in_client, mock_api_client): + """#42: the API sends UTC; the summary shows Europe/Rome.""" + mock_api_client.get.side_effect = [ + {"id": 1, "code": "REC-001", "name": "Test Recipe", "version": 1}, + [{"id": 1, "title": "Task 1", "order_index": 0, "subtasks": [ + {"id": 1, "marker_number": 1, "description": "d", "nominal": 1.0, + "unit": "mm"}]}], + None, + {"items": [{"id": 1, "subtask_id": 1, "value": 1.0, "pass_fail": "pass", + "measured_at": "2026-10-06T08:00:00+00:00"}]}, + ] + resp = logged_in_client.get("/measure/complete/1?version_id=5") + assert resp.status_code == 200 + assert "06/10/2026 10:00" in resp.get_data(as_text=True) + def test_measurements_filtered_by_lot_serial_and_operator( self, logged_in_client, mock_api_client, ):