From 4f1add91542f8994dec75d59f88ca2a108c62209 Mon Sep 17 00:00:00 2001 From: Peter Stockings Date: Mon, 9 Mar 2026 22:13:56 +1100 Subject: [PATCH] Add ability to move forward/back on week/month view and improve UI --- app/routes/main.py | 31 +++++++- app/static/css/tailwind.css | 123 ++++++++++++++++++++++++++++ app/templates/dashboard.html | 150 ++++++++++++++++++++++++----------- 3 files changed, 254 insertions(+), 50 deletions(-) diff --git a/app/routes/main.py b/app/routes/main.py index faf1026..82d4af8 100644 --- a/app/routes/main.py +++ b/app/routes/main.py @@ -34,15 +34,36 @@ def dashboard(): # Pagination for list view page = request.args.get('page', 1, type=int) + + # Weekly View Navigation Offset + week_offset = request.args.get('week_offset', 0, type=int) + + # Monthly View Navigation Offset + month_offset = request.args.get('month_offset', 0, type=int) # Fetch paginated readings for the list view paginated = fetch_readings_paginated(current_user.id, start_date, end_date, user_tz, page, PAGE_SIZE) # For calendar/graph/badges, fetch only current month + week readings (much smaller set) now = datetime.now(user_tz) + + # Calculate target week date for weekly view + target_week_date = now + timedelta(weeks=week_offset) + # Ensure we fetch enough data back to cover the week_offset month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0) month_start_utc = month_start.astimezone(utc) - calendar_readings = fetch_readings_for_range(current_user.id, month_start_utc) + target_week_start = target_week_date - timedelta(days=target_week_date.weekday()) + target_week_start_utc = target_week_start.replace(hour=0, minute=0, second=0, microsecond=0).astimezone(utc) + + # Calculate target month date for monthly view + target_month_year = now.year + (now.month + month_offset - 1) // 12 + target_month_month = (now.month + month_offset - 1) % 12 + 1 + target_month_date = now.replace(year=target_month_year, month=target_month_month, day=1, hour=0, minute=0, second=0, microsecond=0) + target_month_start_utc = target_month_date.astimezone(utc) + + # Fetch from the earliest of the current month OR the requested target week/month + fetch_start_utc = min(month_start_utc, target_week_start_utc, target_month_start_utc) + calendar_readings = fetch_readings_for_range(current_user.id, fetch_start_utc) # Annotate all readings with relative and localized timestamps annotate_readings(paginated.items, user_tz) @@ -52,8 +73,8 @@ def dashboard(): readings_by_day = build_readings_by_day(calendar_readings, user_tz) # Generate calendar views from the shared lookup - week_view = generate_weekly_calendar(readings_by_day, now, user_tz) - month_view = generate_monthly_calendar(readings_by_day, now, user_tz) + week_view = generate_weekly_calendar(readings_by_day, target_week_date, user_tz) + month_view = generate_monthly_calendar(readings_by_day, target_month_date, user_tz) # Calculate weekly averages via SQL (much faster than Python) systolic_avg, diastolic_avg, heart_rate_avg = calculate_weekly_summary_sql(current_user.id) @@ -79,6 +100,10 @@ def dashboard(): month=month_view, week=week_view, date=date, + target_month_date=target_month_date, + week_offset=week_offset, + month_offset=month_offset, + active_view=request.args.get('activeView', 'list'), **graph_data ) diff --git a/app/static/css/tailwind.css b/app/static/css/tailwind.css index 60da864..0121763 100644 --- a/app/static/css/tailwind.css +++ b/app/static/css/tailwind.css @@ -791,6 +791,10 @@ video { margin-left: 0.25rem; } +.mt-0\.5 { + margin-top: 0.125rem; +} + .block { display: block; } @@ -860,6 +864,14 @@ video { height: 0.875rem; } +.min-h-\[140px\] { + min-height: 140px; +} + +.min-h-\[120px\] { + min-height: 120px; +} + .w-16 { width: 4rem; } @@ -1004,6 +1016,14 @@ video { gap: 0.75rem; } +.gap-1 { + gap: 0.25rem; +} + +.gap-0\.5 { + gap: 0.125rem; +} + .space-x-2 > :not([hidden]) ~ :not([hidden]) { --tw-space-x-reverse: 0; margin-right: calc(0.5rem * var(--tw-space-x-reverse)); @@ -1034,10 +1054,26 @@ video { margin-bottom: calc(1.5rem * var(--tw-space-y-reverse)); } +.space-y-1\.5 > :not([hidden]) ~ :not([hidden]) { + --tw-space-y-reverse: 0; + margin-top: calc(0.375rem * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(0.375rem * var(--tw-space-y-reverse)); +} + +.space-y-1 > :not([hidden]) ~ :not([hidden]) { + --tw-space-y-reverse: 0; + margin-top: calc(0.25rem * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(0.25rem * var(--tw-space-y-reverse)); +} + .overflow-hidden { overflow: hidden; } +.whitespace-nowrap { + white-space: nowrap; +} + .rounded { border-radius: 0.25rem; } @@ -1131,6 +1167,11 @@ video { border-color: rgb(13 148 136 / var(--tw-border-opacity, 1)); } +.border-primary-400 { + --tw-border-opacity: 1; + border-color: rgb(45 212 191 / var(--tw-border-opacity, 1)); +} + .bg-blue-600 { --tw-bg-opacity: 1; background-color: rgb(37 99 235 / var(--tw-bg-opacity, 1)); @@ -1225,6 +1266,11 @@ video { background-color: rgb(204 251 241 / var(--tw-bg-opacity, 1)); } +.bg-red-50 { + --tw-bg-opacity: 1; + background-color: rgb(254 242 242 / var(--tw-bg-opacity, 1)); +} + .bg-gradient-to-r { background-image: linear-gradient(to right, var(--tw-gradient-stops)); } @@ -1314,6 +1360,10 @@ video { padding: 2.5rem; } +.p-1\.5 { + padding: 0.375rem; +} + .px-2 { padding-left: 0.5rem; padding-right: 0.5rem; @@ -1374,6 +1424,21 @@ video { padding-bottom: 6rem; } +.px-1 { + padding-left: 0.25rem; + padding-right: 0.25rem; +} + +.px-1\.5 { + padding-left: 0.375rem; + padding-right: 0.375rem; +} + +.py-0\.5 { + padding-top: 0.125rem; + padding-bottom: 0.125rem; +} + .pl-1 { padding-left: 0.25rem; } @@ -1467,6 +1532,14 @@ video { line-height: 1; } +.text-\[10px\] { + font-size: 10px; +} + +.text-\[9px\] { + font-size: 9px; +} + .font-bold { font-weight: 700; } @@ -1495,6 +1568,10 @@ video { line-height: 1.625; } +.leading-none { + line-height: 1; +} + .tracking-tight { letter-spacing: -0.025em; } @@ -1597,6 +1674,10 @@ video { -moz-osx-font-smoothing: grayscale; } +.opacity-50 { + opacity: 0.5; +} + .shadow { --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color); @@ -1684,6 +1765,16 @@ video { transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); } +.hover\:border-primary-400:hover { + --tw-border-opacity: 1; + border-color: rgb(45 212 191 / var(--tw-border-opacity, 1)); +} + +.hover\:border-primary-300:hover { + --tw-border-opacity: 1; + border-color: rgb(94 234 212 / var(--tw-border-opacity, 1)); +} + .hover\:bg-blue-700:hover { --tw-bg-opacity: 1; background-color: rgb(29 78 216 / var(--tw-bg-opacity, 1)); @@ -1739,6 +1830,11 @@ video { background-color: rgb(15 118 110 / var(--tw-bg-opacity, 1)); } +.hover\:bg-primary-50:hover { + --tw-bg-opacity: 1; + background-color: rgb(240 253 250 / var(--tw-bg-opacity, 1)); +} + .hover\:text-gray-200:hover { --tw-text-opacity: 1; color: rgb(229 231 235 / var(--tw-text-opacity, 1)); @@ -1890,6 +1986,15 @@ video { margin-top: calc(0px * calc(1 - var(--tw-space-y-reverse))); margin-bottom: calc(0px * var(--tw-space-y-reverse)); } + + .sm\:text-\[10px\] { + font-size: 10px; + } + + .sm\:text-xs { + font-size: 0.75rem; + line-height: 1rem; + } } @media (min-width: 768px) { @@ -1963,6 +2068,10 @@ video { } @media (min-width: 1280px) { + .xl\:mb-0 { + margin-bottom: 0px; + } + .xl\:block { display: block; } @@ -1970,4 +2079,18 @@ video { .xl\:hidden { display: none; } + + .xl\:flex-row { + flex-direction: row; + } +} + +@media (min-width: 1536px) { + .\32xl\:mt-0 { + margin-top: 0px; + } + + .\32xl\:flex-row { + flex-direction: row; + } } \ No newline at end of file diff --git a/app/templates/dashboard.html b/app/templates/dashboard.html index 489991d..95054dd 100644 --- a/app/templates/dashboard.html +++ b/app/templates/dashboard.html @@ -84,7 +84,7 @@ -
+