Compare commits
3 Commits
de66dc5fd8
...
a0abf41ff6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0abf41ff6 | ||
|
|
4f1add9154 | ||
|
|
31203cd551 |
@@ -24,64 +24,118 @@ def health():
|
||||
@main.route('/dashboard', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def dashboard():
|
||||
"""Render the dashboard with readings, stats, and calendar views."""
|
||||
"""Render the dashboard shell and default list view."""
|
||||
user_tz = timezone(current_user.profile.timezone or 'UTC')
|
||||
|
||||
# Get date range for readings
|
||||
# Get date range for filters
|
||||
first_reading, last_reading = get_reading_date_range(current_user.id, user_tz)
|
||||
start_date = request.form.get('start_date') or (first_reading and first_reading.strftime('%Y-%m-%d'))
|
||||
end_date = request.form.get('end_date') or (last_reading and last_reading.strftime('%Y-%m-%d'))
|
||||
start_date = request.form.get('start_date') or request.args.get('start_date') or (first_reading and first_reading.strftime('%Y-%m-%d'))
|
||||
end_date = request.form.get('end_date') or request.args.get('end_date') or (last_reading and last_reading.strftime('%Y-%m-%d'))
|
||||
|
||||
# Pagination for list view
|
||||
page = request.args.get('page', 1, 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)
|
||||
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)
|
||||
|
||||
# Annotate all readings with relative and localized timestamps
|
||||
annotate_readings(paginated.items, user_tz)
|
||||
annotate_readings(calendar_readings, user_tz)
|
||||
|
||||
# Build shared lookup for calendar views (single pass)
|
||||
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)
|
||||
|
||||
# Calculate weekly averages via SQL (much faster than Python)
|
||||
# Calculate weekly averages via SQL
|
||||
systolic_avg, diastolic_avg, heart_rate_avg = calculate_weekly_summary_sql(current_user.id)
|
||||
|
||||
# Badges from the paginated readings (or full set if needed)
|
||||
badges = calculate_progress_badges(paginated.items)
|
||||
# Badges need paginated or all readings. We'll fetch all readings for badges
|
||||
# Note: To avoid huge queries, we might want a specific badge query in the future.
|
||||
# For now, let's fetch current month readings + previous as a rough approximation or keep behavior.
|
||||
# A generic query for badges:
|
||||
all_readings = Reading.query.filter_by(user_id=current_user.id).order_by(Reading.timestamp.desc()).all()
|
||||
badges = calculate_progress_badges(all_readings)
|
||||
|
||||
# Prepare graph data from calendar readings (current month)
|
||||
graph_data = prepare_graph_data(calendar_readings)
|
||||
# We will default to showing the list view on initial load
|
||||
page = request.args.get('page', 1, type=int)
|
||||
paginated = fetch_readings_paginated(current_user.id, start_date, end_date, user_tz, page, PAGE_SIZE)
|
||||
annotate_readings(paginated.items, user_tz)
|
||||
|
||||
return render_template(
|
||||
'dashboard.html',
|
||||
readings=paginated.items,
|
||||
pagination=paginated,
|
||||
profile=current_user.profile,
|
||||
badges=badges,
|
||||
systolic_avg=systolic_avg,
|
||||
diastolic_avg=diastolic_avg,
|
||||
heart_rate_avg=heart_rate_avg,
|
||||
delete_form=DeleteForm(),
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
month=month_view,
|
||||
week=week_view,
|
||||
date=date,
|
||||
**graph_data
|
||||
delete_form=DeleteForm(),
|
||||
active_view='list',
|
||||
# default view context
|
||||
readings=paginated.items,
|
||||
pagination=paginated
|
||||
)
|
||||
|
||||
@main.route('/dashboard/list', methods=['GET'])
|
||||
@login_required
|
||||
def dashboard_list():
|
||||
user_tz = timezone(current_user.profile.timezone or 'UTC')
|
||||
first_reading, last_reading = get_reading_date_range(current_user.id, user_tz)
|
||||
start_date = request.args.get('start_date') or (first_reading and first_reading.strftime('%Y-%m-%d'))
|
||||
end_date = request.args.get('end_date') or (last_reading and last_reading.strftime('%Y-%m-%d'))
|
||||
page = request.args.get('page', 1, type=int)
|
||||
|
||||
paginated = fetch_readings_paginated(current_user.id, start_date, end_date, user_tz, page, PAGE_SIZE)
|
||||
annotate_readings(paginated.items, user_tz)
|
||||
|
||||
return render_template('partials/dashboard_list.html', readings=paginated.items, pagination=paginated)
|
||||
|
||||
@main.route('/dashboard/weekly', methods=['GET'])
|
||||
@login_required
|
||||
def dashboard_weekly():
|
||||
user_tz = timezone(current_user.profile.timezone or 'UTC')
|
||||
week_offset = request.args.get('week_offset', 0, type=int)
|
||||
now = datetime.now(user_tz)
|
||||
|
||||
target_week_date = now + timedelta(weeks=week_offset)
|
||||
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)
|
||||
target_week_end_utc = (target_week_start + timedelta(days=7)).replace(hour=0, minute=0, second=0, microsecond=0).astimezone(utc)
|
||||
|
||||
calendar_readings = fetch_readings_for_range(current_user.id, target_week_start_utc, target_week_end_utc)
|
||||
annotate_readings(calendar_readings, user_tz)
|
||||
readings_by_day = build_readings_by_day(calendar_readings, user_tz)
|
||||
week_view = generate_weekly_calendar(readings_by_day, target_week_date, user_tz)
|
||||
|
||||
return render_template('partials/dashboard_weekly.html', week=week_view, week_offset=week_offset)
|
||||
|
||||
@main.route('/dashboard/monthly', methods=['GET'])
|
||||
@login_required
|
||||
def dashboard_monthly():
|
||||
user_tz = timezone(current_user.profile.timezone or 'UTC')
|
||||
month_offset = request.args.get('month_offset', 0, type=int)
|
||||
now = datetime.now(user_tz)
|
||||
|
||||
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)
|
||||
|
||||
first_day = target_month_date
|
||||
start_date = first_day - timedelta(days=(first_day.weekday() + 1) % 7)
|
||||
end_date = start_date + timedelta(days=42)
|
||||
|
||||
start_utc = start_date.replace(hour=0, minute=0, second=0, microsecond=0).astimezone(utc)
|
||||
end_utc = end_date.replace(hour=0, minute=0, second=0, microsecond=0).astimezone(utc)
|
||||
|
||||
calendar_readings = fetch_readings_for_range(current_user.id, start_utc, end_utc)
|
||||
annotate_readings(calendar_readings, user_tz)
|
||||
readings_by_day = build_readings_by_day(calendar_readings, user_tz)
|
||||
month_view = generate_monthly_calendar(readings_by_day, target_month_date, user_tz)
|
||||
|
||||
return render_template('partials/dashboard_monthly.html', month=month_view, target_month_date=target_month_date, month_offset=month_offset)
|
||||
|
||||
@main.route('/dashboard/graph', methods=['GET'])
|
||||
@login_required
|
||||
def dashboard_graph():
|
||||
user_tz = timezone(current_user.profile.timezone or 'UTC')
|
||||
now = datetime.now(user_tz)
|
||||
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)
|
||||
annotate_readings(calendar_readings, user_tz)
|
||||
|
||||
graph_data = prepare_graph_data(calendar_readings)
|
||||
|
||||
return render_template('partials/dashboard_graph.html', **graph_data)
|
||||
|
||||
def get_reading_date_range(user_id, user_tz):
|
||||
"""Fetch the earliest and latest reading timestamps for a user."""
|
||||
result = db.session.query(
|
||||
|
||||
@@ -682,6 +682,11 @@ video {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.mx-0\.5 {
|
||||
margin-left: 0.125rem;
|
||||
margin-right: 0.125rem;
|
||||
}
|
||||
|
||||
.-mb-px {
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
@@ -774,6 +779,22 @@ video {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.mb-0\.5 {
|
||||
margin-bottom: 0.125rem;
|
||||
}
|
||||
|
||||
.mb-1 {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.ml-1 {
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.mt-0\.5 {
|
||||
margin-top: 0.125rem;
|
||||
}
|
||||
|
||||
.block {
|
||||
display: block;
|
||||
}
|
||||
@@ -839,6 +860,18 @@ video {
|
||||
height: 0.5rem;
|
||||
}
|
||||
|
||||
.h-3\.5 {
|
||||
height: 0.875rem;
|
||||
}
|
||||
|
||||
.min-h-\[140px\] {
|
||||
min-height: 140px;
|
||||
}
|
||||
|
||||
.min-h-\[120px\] {
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.w-16 {
|
||||
width: 4rem;
|
||||
}
|
||||
@@ -875,6 +908,10 @@ video {
|
||||
width: 5rem;
|
||||
}
|
||||
|
||||
.w-3\.5 {
|
||||
width: 0.875rem;
|
||||
}
|
||||
|
||||
.min-w-\[300px\] {
|
||||
min-width: 300px;
|
||||
}
|
||||
@@ -939,6 +976,10 @@ video {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.items-baseline {
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.justify-end {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
@@ -971,6 +1012,18 @@ video {
|
||||
gap: 2.5rem;
|
||||
}
|
||||
|
||||
.gap-3 {
|
||||
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));
|
||||
@@ -1001,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;
|
||||
}
|
||||
@@ -1098,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));
|
||||
@@ -1192,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));
|
||||
}
|
||||
@@ -1281,6 +1360,10 @@ video {
|
||||
padding: 2.5rem;
|
||||
}
|
||||
|
||||
.p-1\.5 {
|
||||
padding: 0.375rem;
|
||||
}
|
||||
|
||||
.px-2 {
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
@@ -1341,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;
|
||||
}
|
||||
@@ -1381,6 +1479,10 @@ video {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.font-sans {
|
||||
font-family: Inter, sans-serif;
|
||||
}
|
||||
@@ -1430,6 +1532,14 @@ video {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.text-\[10px\] {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.text-\[9px\] {
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
@@ -1446,6 +1556,10 @@ video {
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.font-normal {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.uppercase {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
@@ -1454,6 +1568,10 @@ video {
|
||||
line-height: 1.625;
|
||||
}
|
||||
|
||||
.leading-none {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.tracking-tight {
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
@@ -1542,6 +1660,11 @@ video {
|
||||
color: rgb(153 246 228 / var(--tw-text-opacity, 1));
|
||||
}
|
||||
|
||||
.text-gray-300 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(209 213 219 / var(--tw-text-opacity, 1));
|
||||
}
|
||||
|
||||
.no-underline {
|
||||
text-decoration-line: none;
|
||||
}
|
||||
@@ -1551,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);
|
||||
@@ -1638,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));
|
||||
@@ -1693,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));
|
||||
@@ -1754,6 +1896,12 @@ video {
|
||||
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
||||
}
|
||||
|
||||
.hover\:shadow-md:hover {
|
||||
--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||
--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);
|
||||
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
||||
}
|
||||
|
||||
.focus\:border-blue-500:focus {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(59 130 246 / var(--tw-border-opacity, 1));
|
||||
@@ -1838,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) {
|
||||
@@ -1911,6 +2068,10 @@ video {
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.xl\:mb-0 {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.xl\:block {
|
||||
display: block;
|
||||
}
|
||||
@@ -1918,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;
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link href="/static/css/tailwind.css" rel="stylesheet">
|
||||
<script src="/static/js/alpine.min.js" defer></script>
|
||||
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
||||
<script src="/static/js/turbolinks.min.js"></script>
|
||||
</head>
|
||||
|
||||
|
||||
@@ -84,259 +84,28 @@
|
||||
</div>
|
||||
|
||||
|
||||
<div class="max-w-5xl mx-auto" x-data="{ activeView: 'list' }">
|
||||
<div class="max-w-5xl mx-auto" x-data="{ activeView: '{{ active_view }}' }">
|
||||
<!-- Tabs -->
|
||||
<div class="flex border-b mb-4">
|
||||
<button @click="activeView = 'list'" :class="{'border-primary-600 text-primary-600': activeView === 'list'}"
|
||||
<button @click="activeView = 'list'" hx-get="{{ url_for('main.dashboard_list') }}"
|
||||
hx-target="#dashboard-content" :class="{'border-primary-600 text-primary-600': activeView === 'list'}"
|
||||
class="px-4 py-2 text-sm font-medium border-b-2">List View</button>
|
||||
<button @click="activeView = 'weekly'"
|
||||
:class="{'border-primary-600 text-primary-600': activeView === 'weekly'}"
|
||||
<button @click="activeView = 'weekly'" hx-get="{{ url_for('main.dashboard_weekly') }}"
|
||||
hx-target="#dashboard-content" :class="{'border-primary-600 text-primary-600': activeView === 'weekly'}"
|
||||
class="px-4 py-2 text-sm font-medium border-b-2">Weekly View</button>
|
||||
<button @click="activeView = 'monthly'"
|
||||
<button @click="activeView = 'monthly'" hx-get="{{ url_for('main.dashboard_monthly') }}"
|
||||
hx-target="#dashboard-content"
|
||||
:class="{'border-primary-600 text-primary-600': activeView === 'monthly'}"
|
||||
class="px-4 py-2 text-sm font-medium border-b-2">Monthly View</button>
|
||||
<button @click="activeView = 'graph'"
|
||||
:class="{'border-primary-600 text-primary-600': activeView === 'graph'}"
|
||||
<button @click="activeView = 'graph'" hx-get="{{ url_for('main.dashboard_graph') }}"
|
||||
hx-target="#dashboard-content" :class="{'border-primary-600 text-primary-600': activeView === 'graph'}"
|
||||
class="px-4 py-2 text-sm font-medium border-b-2">Graph View</button>
|
||||
</div>
|
||||
|
||||
<!-- List -->
|
||||
<div x-show="activeView === 'list'" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{% for reading in readings %}
|
||||
<a href="{{ url_for('reading.edit_reading', reading_id=reading.id) }}"
|
||||
class="bg-white shadow-md rounded-xl p-4 flex flex-col justify-between relative hover:shadow-lg transition-shadow">
|
||||
<!-- Timestamp -->
|
||||
<div class="absolute top-2 right-2 flex items-center text-gray-400 text-xs">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 mr-1" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M12 8v4l3 3m9-3a9 9 0 1 1-18 0 9 9 0 0 1 18 0z" />
|
||||
</svg>
|
||||
<span title="{{ reading.local_timestamp.strftime('%d %b %Y, %I:%M %p') }}">
|
||||
{{ reading.relative_timestamp }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Blood Pressure -->
|
||||
<div class="text-sm text-gray-600 mb-2">
|
||||
<span class="block text-lg font-semibold text-gray-800">Blood Pressure</span>
|
||||
<span class="text-2xl font-bold text-primary-600">{{ reading.systolic }}</span>
|
||||
<span class="text-lg text-gray-500">/</span>
|
||||
<span class="text-xl font-bold text-red-600">{{ reading.diastolic }}</span>
|
||||
<span class="text-sm text-gray-500">mmHg</span>
|
||||
</div>
|
||||
|
||||
<!-- Heart Rate and Arrow -->
|
||||
<div class="flex justify-between items-center mt-4 relative">
|
||||
<div class="text-sm text-gray-600">
|
||||
<span class="block text-lg font-semibold text-gray-800">Heart Rate</span>
|
||||
<span class="text-2xl font-bold text-green-600">{{ reading.heart_rate }}</span>
|
||||
<span class="text-sm text-gray-500">bpm</span>
|
||||
</div>
|
||||
<!-- Arrow Icon -->
|
||||
<div class="absolute bottom-0 right-0 text-gray-400 hover:text-gray-600">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
stroke="currentColor" class="h-5 w-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M6.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0ZM12.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0ZM18.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Z" />
|
||||
</svg>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{% else %}
|
||||
<div class="col-span-full text-center text-sm text-gray-500">
|
||||
No readings found.
|
||||
</div>
|
||||
{% endfor %}
|
||||
<!-- Dashboard Content Target Area for HTMX -->
|
||||
<div id="dashboard-content">
|
||||
{% include 'partials/dashboard_list.html' %}
|
||||
</div>
|
||||
|
||||
<!-- Pagination Controls -->
|
||||
{% if pagination.pages > 1 %}
|
||||
<div x-show="activeView === 'list'" class="flex justify-center items-center gap-2 mt-6">
|
||||
{% if pagination.has_prev %}
|
||||
<a href="{{ url_for('main.dashboard', page=pagination.prev_num) }}"
|
||||
class="px-3 py-1 rounded bg-gray-200 hover:bg-gray-300 text-sm">« Prev</a>
|
||||
{% endif %}
|
||||
|
||||
{% for page_num in pagination.iter_pages(left_edge=1, right_edge=1, left_current=2, right_current=2) %}
|
||||
{% if page_num %}
|
||||
<a href="{{ url_for('main.dashboard', page=page_num) }}"
|
||||
class="px-3 py-1 rounded text-sm {% if page_num == pagination.page %}bg-primary-600 text-white{% else %}bg-gray-200 hover:bg-gray-300{% endif %}">
|
||||
{{ page_num }}
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="text-gray-400">…</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if pagination.has_next %}
|
||||
<a href="{{ url_for('main.dashboard', page=pagination.next_num) }}"
|
||||
class="px-3 py-1 rounded bg-gray-200 hover:bg-gray-300 text-sm">Next »</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Weekly View -->
|
||||
<div x-show="activeView === 'weekly'" class="grid grid-cols-7 text-center">
|
||||
{% for day in week %}
|
||||
<div class="border p-1 md:p-4 bg-gray-50">
|
||||
<div class="text-sm font-bold text-gray-500">{{ day.date }}</div>
|
||||
{% if day.readings %}
|
||||
{% for reading in day.readings %}
|
||||
<a href="{{ url_for('reading.edit_reading', reading_id=reading.id) }}"
|
||||
class="block mt-2 p-0 md:p-2 bg-green-100 rounded-xl shadow hover:bg-green-200 transition">
|
||||
<p class="text-xs font-medium text-green-800">
|
||||
{{ reading.systolic }}/{{ reading.diastolic }} mmHg
|
||||
</p>
|
||||
<p class="text-xs text-gray-600 mt-1">{{ reading.heart_rate }} bpm</p>
|
||||
<!-- Timestamp -->
|
||||
<div class="text-xs text-gray-500 mt-1">
|
||||
{{ reading.local_timestamp.strftime('%I:%M %p') }}
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="h-8"></div> <!-- Placeholder for spacing -->
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Monthly View -->
|
||||
<div class="flex flex-col px-2 py-2 -mb-px" x-show="activeView === 'monthly'">
|
||||
{% set current_date = date.today().replace(day=1) %}
|
||||
<!-- Month Name -->
|
||||
<div class="text-center">
|
||||
<h2 class="text-xl font-bold text-gray-800">{{ current_date.strftime('%B %Y') }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-7 pl-2 pr-2">
|
||||
|
||||
<div class="p-2 h-10 text-center font-bold">
|
||||
<span class="xl:block lg:block md:block sm:block hidden">Sunday</span>
|
||||
<span class="xl:hidden lg:hidden md:hidden sm:hidden block">Sun</span>
|
||||
</div>
|
||||
<div class="p-2 h-10 text-center font-bold">
|
||||
<span class="xl:block lg:block md:block sm:block hidden">Monday</span>
|
||||
<span class="xl:hidden lg:hidden md:hidden sm:hidden block">Mon</span>
|
||||
</div>
|
||||
<div class="p-2 h-10 text-center font-bold">
|
||||
<span class="xl:block lg:block md:block sm:block hidden">Tuesday</span>
|
||||
<span class="xl:hidden lg:hidden md:hidden sm:hidden block">Tue</span>
|
||||
</div>
|
||||
<div class="p-2 h-10 text-center font-bold">
|
||||
<span class="xl:block lg:block md:block sm:block hidden">Wednesday</span>
|
||||
<span class="xl:hidden lg:hidden md:hidden sm:hidden block">Wed</span>
|
||||
</div>
|
||||
<div class="p-2 h-10 text-center font-bold">
|
||||
<span class="xl:block lg:block md:block sm:block hidden">Thursday</span>
|
||||
<span class="xl:hidden lg:hidden md:hidden sm:hidden block">Thu</span>
|
||||
</div>
|
||||
<div class="p-2 h-10 text-center font-bold">
|
||||
<span class="xl:block lg:block md:block sm:block hidden">Friday</span>
|
||||
<span class="xl:hidden lg:hidden md:hidden sm:hidden block">Fri</span>
|
||||
</div>
|
||||
<div class="p-2 h-10 text-center font-bold">
|
||||
<span class="xl:block lg:block md:block sm:block hidden">Saturday</span>
|
||||
<span class="xl:hidden lg:hidden md:hidden sm:hidden block">Sat</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-7 overflow-hidden flex-1 pl-2 pr-2 w-full">
|
||||
|
||||
{% for day in month %}
|
||||
<div
|
||||
class="{% if day.is_today %}rounded-md border-4 border-green-50{% endif %} border flex flex-col h-36 sm:h-40 md:h-30 lg:h-30 mx-auto mx-auto overflow-hidden w-full pt-2 pl-1 cursor-pointer {% if day.is_in_current_month %}bg-gray-100{% endif %}">
|
||||
<div class="top h-5 w-full">
|
||||
<span class="text-gray-500 font-semibold">{{ day.day }}</span>
|
||||
</div>
|
||||
{% for reading in day.readings %}
|
||||
<a href="{{ url_for('reading.edit_reading', reading_id=reading.id) }}"
|
||||
class="block mt-2 p-0 md:p-2 bg-green-100 rounded-xl shadow hover:bg-green-200 transition">
|
||||
<p class="text-xs font-medium text-green-800">
|
||||
{{ reading.systolic }}/{{ reading.diastolic }} mmHg
|
||||
</p>
|
||||
<p class="text-xs text-gray-600 mt-1">{{ reading.heart_rate }} bpm</p>
|
||||
<!-- Timestamp -->
|
||||
<div class="text-xs text-gray-500 mt-1">
|
||||
{{ reading.local_timestamp.strftime('%I:%M %p') }}
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Graph Section -->
|
||||
<div x-show="activeView === 'graph'" class="space-y-6">
|
||||
<!-- Blood Pressure Graph -->
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-gray-600 mb-2">Blood Pressure (mmHg)</h3>
|
||||
<svg viewBox="0 0 600 250" xmlns="http://www.w3.org/2000/svg" class="w-full">
|
||||
<!-- Axes -->
|
||||
<line x1="50" y1="200" x2="550" y2="200" stroke="black" stroke-width="1" /> <!-- X-axis -->
|
||||
<line x1="50" y1="20" x2="50" y2="200" stroke="black" stroke-width="1" /> <!-- Y-axis -->
|
||||
|
||||
<!-- Y-axis Labels (Blood Pressure Values) -->
|
||||
{% for value in range(50, 201, 50) %}
|
||||
<text x="40" y="{{ 200 - (value / 200 * 180) }}" font-size="10" text-anchor="end">{{ value }}</text>
|
||||
{% endfor %}
|
||||
|
||||
<!-- X-axis Labels (Timestamps) -->
|
||||
{% for i in range(timestamps|length) %}
|
||||
<text x="{{ 50 + i * 50 }}" y="215" font-size="10" text-anchor="middle">{{ timestamps[i] }}</text>
|
||||
{% endfor %}
|
||||
|
||||
<!-- Graph Lines -->
|
||||
<!-- Systolic Line -->
|
||||
<polyline fill="none" stroke="blue" stroke-width="2"
|
||||
points="{% for i in range(timestamps|length) %}{{ 50 + i * 50 }},{{ 200 - (systolic[i] / 200 * 180) }} {% endfor %}" />
|
||||
|
||||
<!-- Diastolic Line -->
|
||||
<polyline fill="none" stroke="red" stroke-width="2"
|
||||
points="{% for i in range(timestamps|length) %}{{ 50 + i * 50 }},{{ 200 - (diastolic[i] / 200 * 180) }} {% endfor %}" />
|
||||
|
||||
<!-- Axis Labels -->
|
||||
<text x="25" y="110" font-size="12" transform="rotate(-90, 25, 110)" text-anchor="middle">Blood
|
||||
Pressure (mmHg)</text>
|
||||
<text x="300" y="240" font-size="12" text-anchor="middle">Date</text>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- Heart Rate Graph -->
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-gray-600 mb-2">Heart Rate (bpm)</h3>
|
||||
<svg viewBox="0 0 600 250" xmlns="http://www.w3.org/2000/svg" class="w-full">
|
||||
<!-- Axes -->
|
||||
<line x1="50" y1="200" x2="550" y2="200" stroke="black" stroke-width="1" /> <!-- X-axis -->
|
||||
<line x1="50" y1="20" x2="50" y2="200" stroke="black" stroke-width="1" /> <!-- Y-axis -->
|
||||
|
||||
<!-- Y-axis Labels (Heart Rate Values) -->
|
||||
{% for value in range(50, 201, 50) %}
|
||||
<text x="40" y="{{ 200 - (value / 200 * 180) }}" font-size="10" text-anchor="end">{{ value }}</text>
|
||||
{% endfor %}
|
||||
|
||||
<!-- X-axis Labels (Timestamps) -->
|
||||
{% for i in range(timestamps|length) %}
|
||||
<text x="{{ 50 + i * 50 }}" y="215" font-size="10" text-anchor="middle">{{ timestamps[i] }}</text>
|
||||
{% endfor %}
|
||||
|
||||
<!-- Heart Rate Line -->
|
||||
<polyline fill="none" stroke="green" stroke-width="2"
|
||||
points="{% for i in range(timestamps|length) %}{{ 50 + i * 50 }},{{ 200 - (heart_rate[i] / 200 * 180) }} {% endfor %}" />
|
||||
|
||||
<!-- Axis Labels -->
|
||||
<text x="25" y="110" font-size="12" transform="rotate(-90, 25, 110)" text-anchor="middle">Heart Rate
|
||||
(bpm)</text>
|
||||
<text x="300" y="240" font-size="12" text-anchor="middle">Date</text>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
64
app/templates/partials/dashboard_graph.html
Normal file
64
app/templates/partials/dashboard_graph.html
Normal file
@@ -0,0 +1,64 @@
|
||||
<div class="space-y-6">
|
||||
<!-- Blood Pressure Graph -->
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-gray-600 mb-2">Blood Pressure (mmHg)</h3>
|
||||
<svg viewBox="0 0 600 250" xmlns="http://www.w3.org/2000/svg" class="w-full">
|
||||
<!-- Axes -->
|
||||
<line x1="50" y1="200" x2="550" y2="200" stroke="black" stroke-width="1" /> <!-- X-axis -->
|
||||
<line x1="50" y1="20" x2="50" y2="200" stroke="black" stroke-width="1" /> <!-- Y-axis -->
|
||||
|
||||
<!-- Y-axis Labels (Blood Pressure Values) -->
|
||||
{% for value in range(50, 201, 50) %}
|
||||
<text x="40" y="{{ 200 - (value / 200 * 180) }}" font-size="10" text-anchor="end">{{ value }}</text>
|
||||
{% endfor %}
|
||||
|
||||
<!-- X-axis Labels (Timestamps) -->
|
||||
{% for i in range(timestamps|length) %}
|
||||
<text x="{{ 50 + i * 50 }}" y="215" font-size="10" text-anchor="middle">{{ timestamps[i] }}</text>
|
||||
{% endfor %}
|
||||
|
||||
<!-- Graph Lines -->
|
||||
<!-- Systolic Line -->
|
||||
<polyline fill="none" stroke="blue" stroke-width="2"
|
||||
points="{% for i in range(timestamps|length) %}{{ 50 + i * 50 }},{{ 200 - (systolic[i] / 200 * 180) }} {% endfor %}" />
|
||||
|
||||
<!-- Diastolic Line -->
|
||||
<polyline fill="none" stroke="red" stroke-width="2"
|
||||
points="{% for i in range(timestamps|length) %}{{ 50 + i * 50 }},{{ 200 - (diastolic[i] / 200 * 180) }} {% endfor %}" />
|
||||
|
||||
<!-- Axis Labels -->
|
||||
<text x="25" y="110" font-size="12" transform="rotate(-90, 25, 110)" text-anchor="middle">Blood Pressure
|
||||
(mmHg)</text>
|
||||
<text x="300" y="240" font-size="12" text-anchor="middle">Date</text>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- Heart Rate Graph -->
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-gray-600 mb-2">Heart Rate (bpm)</h3>
|
||||
<svg viewBox="0 0 600 250" xmlns="http://www.w3.org/2000/svg" class="w-full">
|
||||
<!-- Axes -->
|
||||
<line x1="50" y1="200" x2="550" y2="200" stroke="black" stroke-width="1" /> <!-- X-axis -->
|
||||
<line x1="50" y1="20" x2="50" y2="200" stroke="black" stroke-width="1" /> <!-- Y-axis -->
|
||||
|
||||
<!-- Y-axis Labels (Heart Rate Values) -->
|
||||
{% for value in range(50, 201, 50) %}
|
||||
<text x="40" y="{{ 200 - (value / 200 * 180) }}" font-size="10" text-anchor="end">{{ value }}</text>
|
||||
{% endfor %}
|
||||
|
||||
<!-- X-axis Labels (Timestamps) -->
|
||||
{% for i in range(timestamps|length) %}
|
||||
<text x="{{ 50 + i * 50 }}" y="215" font-size="10" text-anchor="middle">{{ timestamps[i] }}</text>
|
||||
{% endfor %}
|
||||
|
||||
<!-- Heart Rate Line -->
|
||||
<polyline fill="none" stroke="green" stroke-width="2"
|
||||
points="{% for i in range(timestamps|length) %}{{ 50 + i * 50 }},{{ 200 - (heart_rate[i] / 200 * 180) }} {% endfor %}" />
|
||||
|
||||
<!-- Axis Labels -->
|
||||
<text x="25" y="110" font-size="12" transform="rotate(-90, 25, 110)" text-anchor="middle">Heart Rate
|
||||
(bpm)</text>
|
||||
<text x="300" y="240" font-size="12" text-anchor="middle">Date</text>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
73
app/templates/partials/dashboard_list.html
Normal file
73
app/templates/partials/dashboard_list.html
Normal file
@@ -0,0 +1,73 @@
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
{% for reading in readings %}
|
||||
<a href="{{ url_for('reading.edit_reading', reading_id=reading.id) }}"
|
||||
class="bg-white shadow-sm hover:shadow-md rounded-xl p-3 flex justify-between items-center border border-gray-100 transition-all">
|
||||
|
||||
<!-- Left side: Timestamp & BP -->
|
||||
<div>
|
||||
<div class="flex items-center text-gray-400 text-xs mb-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-3.5 h-3.5 mr-1" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M12 8v4l3 3m9-3a9 9 0 1 1-18 0 9 9 0 0 1 18 0z" />
|
||||
</svg>
|
||||
<span title="{{ reading.local_timestamp.strftime('%d %b %Y, %I:%M %p') }}">
|
||||
{{ reading.relative_timestamp }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-baseline">
|
||||
<span class="text-xl font-bold text-gray-800">{{ reading.systolic }}<span
|
||||
class="text-gray-400 font-normal mx-0.5">/</span>{{ reading.diastolic }}</span>
|
||||
<span class="text-xs text-gray-500 ml-1">mmHg</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right side: Heart Rate & Icon -->
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="text-right">
|
||||
<span class="text-xs text-gray-500 block mb-0.5">HR</span>
|
||||
<div class="flex items-baseline justify-end">
|
||||
<span class="text-lg font-bold text-gray-700">{{ reading.heart_rate }}</span>
|
||||
<span class="text-xs text-gray-400 ml-1">bpm</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-gray-300">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2"
|
||||
stroke="currentColor" class="h-4 w-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{% else %}
|
||||
<div class="col-span-full text-center text-sm text-gray-500">
|
||||
No readings found.
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Pagination Controls -->
|
||||
{% if pagination.pages > 1 %}
|
||||
<div class="flex justify-center items-center gap-2 mt-6">
|
||||
{% if pagination.has_prev %}
|
||||
<button hx-get="{{ url_for('main.dashboard_list', page=pagination.prev_num) }}" hx-target="#dashboard-content"
|
||||
class="px-3 py-1 rounded bg-gray-200 hover:bg-gray-300 text-sm">« Prev</button>
|
||||
{% endif %}
|
||||
|
||||
{% for page_num in pagination.iter_pages(left_edge=1, right_edge=1, left_current=2, right_current=2) %}
|
||||
{% if page_num %}
|
||||
<button hx-get="{{ url_for('main.dashboard_list', page=page_num) }}" hx-target="#dashboard-content"
|
||||
class="px-3 py-1 rounded text-sm {% if page_num == pagination.page %}bg-primary-600 text-white{% else %}bg-gray-200 hover:bg-gray-300{% endif %}">
|
||||
{{ page_num }}
|
||||
</button>
|
||||
{% else %}
|
||||
<span class="text-gray-400">…</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if pagination.has_next %}
|
||||
<button hx-get="{{ url_for('main.dashboard_list', page=pagination.next_num) }}" hx-target="#dashboard-content"
|
||||
class="px-3 py-1 rounded bg-gray-200 hover:bg-gray-300 text-sm">Next »</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
88
app/templates/partials/dashboard_monthly.html
Normal file
88
app/templates/partials/dashboard_monthly.html
Normal file
@@ -0,0 +1,88 @@
|
||||
<div class="flex flex-col px-2 py-2 -mb-px">
|
||||
<!-- Monthly Navigation -->
|
||||
<div class="flex justify-between items-center mb-4 px-2 mt-2">
|
||||
<button hx-get="{{ url_for('main.dashboard_monthly', month_offset=month_offset - 1) }}"
|
||||
hx-target="#dashboard-content"
|
||||
class="flex items-center text-primary-600 hover:text-primary-800 font-medium transition-colors">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-1" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
Previous Month
|
||||
</button>
|
||||
|
||||
<h2 class="text-xl font-bold text-gray-800">{{ target_month_date.strftime('%B %Y') }}</h2>
|
||||
|
||||
<button hx-get="{{ url_for('main.dashboard_monthly', month_offset=month_offset + 1) }}"
|
||||
hx-target="#dashboard-content"
|
||||
class="flex items-center text-primary-600 hover:text-primary-800 font-medium transition-colors {% if month_offset >= 0 %}opacity-50 pointer-events-none{% endif %}">
|
||||
Next Month
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 ml-1" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-7 pl-2 pr-2">
|
||||
|
||||
<div class="p-2 h-10 text-center font-bold">
|
||||
<span class="xl:block lg:block md:block sm:block hidden">Sunday</span>
|
||||
<span class="xl:hidden lg:hidden md:hidden sm:hidden block">Sun</span>
|
||||
</div>
|
||||
<div class="p-2 h-10 text-center font-bold">
|
||||
<span class="xl:block lg:block md:block sm:block hidden">Monday</span>
|
||||
<span class="xl:hidden lg:hidden md:hidden sm:hidden block">Mon</span>
|
||||
</div>
|
||||
<div class="p-2 h-10 text-center font-bold">
|
||||
<span class="xl:block lg:block md:block sm:block hidden">Tuesday</span>
|
||||
<span class="xl:hidden lg:hidden md:hidden sm:hidden block">Tue</span>
|
||||
</div>
|
||||
<div class="p-2 h-10 text-center font-bold">
|
||||
<span class="xl:block lg:block md:block sm:block hidden">Wednesday</span>
|
||||
<span class="xl:hidden lg:hidden md:hidden sm:hidden block">Wed</span>
|
||||
</div>
|
||||
<div class="p-2 h-10 text-center font-bold">
|
||||
<span class="xl:block lg:block md:block sm:block hidden">Thursday</span>
|
||||
<span class="xl:hidden lg:hidden md:hidden sm:hidden block">Thu</span>
|
||||
</div>
|
||||
<div class="p-2 h-10 text-center font-bold">
|
||||
<span class="xl:block lg:block md:block sm:block hidden">Friday</span>
|
||||
<span class="xl:hidden lg:hidden md:hidden sm:hidden block">Fri</span>
|
||||
</div>
|
||||
<div class="p-2 h-10 text-center font-bold">
|
||||
<span class="xl:block lg:block md:block sm:block hidden">Saturday</span>
|
||||
<span class="xl:hidden lg:hidden md:hidden sm:hidden block">Sat</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-7 overflow-hidden flex-1 pl-2 pr-2 w-full">
|
||||
|
||||
{% for day in month %}
|
||||
<div
|
||||
class="{% if day.is_today %}border-2 border-primary-400 bg-primary-50{% else %}border bg-white{% endif %} flex flex-col min-h-[120px] p-1.5 transition-colors {% if not day.is_in_current_month %}bg-gray-50 opacity-50{% endif %}">
|
||||
<div class="text-right w-full mb-1">
|
||||
<span
|
||||
class="text-xs font-semibold {% if day.is_today %}text-primary-600{% else %}text-gray-500{% endif %}">{{
|
||||
day.day }}</span>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1 flex-grow">
|
||||
{% for reading in day.readings %}
|
||||
<a href="{{ url_for('reading.edit_reading', reading_id=reading.id) }}"
|
||||
class="flex flex-col xl:flex-row justify-between items-center px-1 py-1 bg-white border border-gray-100 rounded shadow-sm hover:border-primary-300 hover:bg-primary-50 transition-colors">
|
||||
<span class="text-[10px] sm:text-xs font-bold text-gray-800 leading-none mb-0.5 xl:mb-0">{{
|
||||
reading.systolic }}/{{ reading.diastolic }}</span>
|
||||
<div class="flex items-center gap-0.5">
|
||||
<span
|
||||
class="text-[9px] sm:text-[10px] font-medium bg-red-50 text-red-600 px-1 rounded leading-none py-0.5"
|
||||
title="Heart Rate">{{ reading.heart_rate }}</span>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
56
app/templates/partials/dashboard_weekly.html
Normal file
56
app/templates/partials/dashboard_weekly.html
Normal file
@@ -0,0 +1,56 @@
|
||||
<div>
|
||||
<!-- Weekly Navigation -->
|
||||
<div class="flex justify-between items-center mb-4 px-2">
|
||||
<button hx-get="{{ url_for('main.dashboard_weekly', week_offset=week_offset - 1) }}"
|
||||
hx-target="#dashboard-content"
|
||||
class="flex items-center text-primary-600 hover:text-primary-800 font-medium transition-colors">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-1" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
Previous Week
|
||||
</button>
|
||||
|
||||
<span class="text-gray-600 font-semibold {% if week_offset == 0 %}text-primary-600{% endif %}">
|
||||
{% if week_offset == 0 %}This Week{% elif week_offset == -1 %}Last Week{% elif week_offset == 1 %}Next
|
||||
Week{% else %}{{ week_offset|abs }} weeks {% if week_offset < 0 %}ago{% else %}from now{% endif %}{% endif
|
||||
%} </span>
|
||||
|
||||
<button hx-get="{{ url_for('main.dashboard_weekly', week_offset=week_offset + 1) }}"
|
||||
hx-target="#dashboard-content"
|
||||
class="flex items-center text-primary-600 hover:text-primary-800 font-medium transition-colors {% if week_offset >= 0 %}opacity-50 pointer-events-none{% endif %}">
|
||||
Next Week
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 ml-1" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-7 text-center">
|
||||
{% for day in week %}
|
||||
<div class="border p-2 bg-gray-50 flex flex-col min-h-[140px]">
|
||||
<div class="text-sm font-bold text-gray-500 mb-2">{{ day.date }}</div>
|
||||
{% if day.readings %}
|
||||
<div class="space-y-1.5 flex-grow">
|
||||
{% for reading in day.readings %}
|
||||
<a href="{{ url_for('reading.edit_reading', reading_id=reading.id) }}"
|
||||
class="flex flex-col 2xl:flex-row justify-between items-center px-1.5 py-1 bg-white border border-gray-200 rounded shadow-sm hover:border-primary-400 hover:bg-primary-50 transition-colors">
|
||||
<span class="text-xs font-bold text-gray-800">{{ reading.systolic }}/{{ reading.diastolic
|
||||
}}</span>
|
||||
<div class="flex items-center gap-1 2xl:mt-0 mt-0.5">
|
||||
<span class="text-[10px] font-medium bg-red-50 text-red-600 px-1 rounded" title="Heart Rate">{{
|
||||
reading.heart_rate }}</span>
|
||||
<span class="text-[10px] text-gray-400 font-medium whitespace-nowrap">{{
|
||||
reading.local_timestamp.strftime('%H:%M') }}</span>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="flex-grow"></div> <!-- Spacer -->
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user