diff --git a/app.py b/app.py index 40397d0..ceacf75 100644 --- a/app.py +++ b/app.py @@ -1,7 +1,17 @@ import os import sqlite3 import json -from datetime import datetime +from datetime import datetime, timezone, timedelta + +EST = timezone(timedelta(hours=-5)) + + +def to_est(dt_str): + """Parse ISO timestamp and convert to EST.""" + dt = datetime.fromisoformat(dt_str) + if dt.tzinfo is None: + dt = dt.replace(tzinfo=timezone.utc) + return dt.astimezone(EST) from collections import defaultdict import csv import io @@ -39,7 +49,7 @@ def compute_wrapped(subset_rows, inflation_adjust=False): sub = float(r["SUBTOTAL"]) if inflation_adjust: try: - yr = str(datetime.fromisoformat(r["CREATED_AT"]).year) + yr = str(to_est(r["CREATED_AT"]).year) except (ValueError, TypeError): yr = "2026" sub *= CPI_TO_2026.get(yr, 1.0) @@ -47,7 +57,7 @@ def compute_wrapped(subset_rows, inflation_adjust=False): ss[r["STORE_NAME"]] += sub ic[r["ITEM"]] += 1 try: - dt = datetime.fromisoformat(r["CREATED_AT"]) + dt = to_est(r["CREATED_AT"]) except (ValueError, TypeError): continue orders.add(r["CREATED_AT"]) @@ -65,7 +75,7 @@ def compute_wrapped(subset_rows, inflation_adjust=False): "fav_store_name": fs[0], "fav_store_total": f"${fs[1]:,.2f}", "fav_store_pct": round(fs[1] / t_spent * 100, 1) if t_spent else 0, - "peak_hour_label": f"{ph[0]}:00", + "peak_hour_label": f"{ph[0]}:00 EST", "peak_hour_count": ph[1], "peak_day_name": pd[0], "biggest_month_label": datetime.strptime(bm[0], "%Y-%m").strftime("%B %Y"), @@ -93,7 +103,7 @@ def render_dashboard(rows): for r in rows: store_spend[r["STORE_NAME"]] += float(r["SUBTOTAL"]) try: - dt = datetime.fromisoformat(r["CREATED_AT"]) + dt = to_est(r["CREATED_AT"]) except (ValueError, TypeError): continue total_orders_set.add(r["CREATED_AT"]) @@ -108,7 +118,7 @@ def render_dashboard(rows): year_data = defaultdict(list) for r in rows: try: - dt = datetime.fromisoformat(r["CREATED_AT"]) + dt = to_est(r["CREATED_AT"]) year_data[str(dt.year)].append(r) except (ValueError, TypeError): pass @@ -128,7 +138,7 @@ def render_dashboard(rows): hour_labels = list(range(24)) hour_values = [hour_counts.get(h, 0) for h in hour_labels] - hour_labels_str = [f"{h}:00" for h in hour_labels] + hour_labels_str = [f"{h}:00 EST" for h in hour_labels] day_order = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] weekday_values = [weekday_counts.get(d, 0) for d in day_order] @@ -275,7 +285,7 @@ HOME_TEMPLATE = """ .btn-primary:disabled { background: #aaa; cursor: not-allowed; } .btn-outline { background: transparent; color: #3e7c17; border: 2px solid #3e7c17; } .btn-outline:hover { background: #3e7c17; color: #fff; } - .or-divider { color: #c4a96a; font-size: 13px; font-weight: 700; margin: 8px 0; } + @@ -305,13 +315,6 @@ HOME_TEMPLATE = """ -
or
- -
-

View Saved Data

-

Browse the dashboard with previously imported order history.

- Open Dashboard -
@@ -464,6 +467,42 @@ TEMPLATE = """
+
+
+
+

Weight Tracker

+

See how your spending correlates with your weight

+
+
+ +
+
+
+
+
+
+ + +
+
+ + +
+ +
+
+ + + Format: date,weight (e.g. 2025-01-15,172.3) +
+
+
+
+
+
+
@@ -498,17 +537,12 @@ TEMPLATE = """ marker: { color: '#6abf69', line: { color: '#3e7c17', width: 1.5 } }, }], { ...layout_base, title: { text: 'Top Stores by Spend', font: { size: 16 } }, yaxis: { autorange: 'reversed' }, margin: { ...layout_base.margin, l: 180 } }, cfg); - Plotly.newPlot('monthly', [{ - x: {{ month_labels|safe }}, y: {{ month_totals|safe }}, - type: 'bar', - marker: { color: '#7ecce5', line: { color: '#4a9bb5', width: 1.5 } }, - }], { ...layout_base, title: { text: 'Monthly Spending', font: { size: 16 } } }, cfg); Plotly.newPlot('hourly', [{ x: {{ hour_labels|safe }}, y: {{ hour_values|safe }}, type: 'bar', marker: { color: '#f4c842', line: { color: '#c4a96a', width: 1.5 } }, - }], { ...layout_base, title: { text: 'Orders by Hour', font: { size: 16 } } }, cfg); + }], { ...layout_base, title: { text: 'Orders by Hour (EST)', font: { size: 16 } } }, cfg); Plotly.newPlot('weekday', [{ x: {{ day_order|safe }}, y: {{ weekday_values|safe }}, @@ -571,6 +605,154 @@ TEMPLATE = """ inflationToggle.addEventListener('change', () => renderWrapped(yearSelect.value)); renderWrapped('All Time'); + // Weight tracking + const WEIGHT_KEY = 'weakness_weight_log'; + function getWeights() { + return JSON.parse(localStorage.getItem(WEIGHT_KEY) || '[]'); + } + function saveWeights(w) { + localStorage.setItem(WEIGHT_KEY, JSON.stringify(w)); + } + function addWeight() { + const dateInput = document.getElementById('weight-date'); + const valInput = document.getElementById('weight-value'); + const d = dateInput.value; + const v = parseFloat(valInput.value); + if (!d || isNaN(v)) return; + const weights = getWeights(); + // Remove existing entry for same date + const filtered = weights.filter(w => w.date !== d); + filtered.push({ date: d, weight: v }); + filtered.sort((a, b) => a.date.localeCompare(b.date)); + saveWeights(filtered); + valInput.value = ''; + renderWeightLog(); + updateMonthlyChart(); + } + document.getElementById('weight-csv-input').addEventListener('change', function(e) { + const file = e.target.files[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = function(ev) { + const lines = ev.target.result.trim().split(String.fromCharCode(10)); + const weights = getWeights(); + const existing = new Set(weights.map(w => w.date)); + let added = 0; + for (const line of lines) { + const parts = line.split(',').map(s => s.trim()); + if (parts.length < 2) continue; + const date = parts[0]; + const val = parseFloat(parts[1]); + // Skip header rows or invalid data + if (!date.match(/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/) || isNaN(val)) continue; + // Overwrite existing entry for same date + const idx = weights.findIndex(w => w.date === date); + if (idx >= 0) weights[idx].weight = val; + else weights.push({ date, weight: val }); + added++; + } + weights.sort((a, b) => a.date.localeCompare(b.date)); + saveWeights(weights); + renderWeightLog(); + updateMonthlyChart(); + alert(`Imported ${added} weight entries.`); + }; + reader.readAsText(file); + e.target.value = ''; + }); + + function removeWeight(date) { + const weights = getWeights().filter(w => w.date !== date); + saveWeights(weights); + renderWeightLog(); + updateMonthlyChart(); + } + function renderWeightLog() { + const log = document.getElementById('weight-log'); + const weights = getWeights(); + if (!weights.length) { + log.innerHTML = '
No weight entries yet. Add your first one!
'; + return; + } + log.innerHTML = weights.map(w => + `
+ ${w.date}: ${w.weight} lbs + +
` + ).join(''); + } + + // Monthly chart with optional weight overlay + const monthLabels = {{ month_labels|safe }}; + const monthTotals = {{ month_totals|safe }}; + + function updateMonthlyChart() { + const showWeight = document.getElementById('weight-overlay-toggle').checked; + const weights = getWeights(); + + // Aggregate weight by month (average per month) + const monthWeights = {}; + const monthWeightCounts = {}; + weights.forEach(w => { + const m = w.date.substring(0, 7); // YYYY-MM + if (!monthWeights[m]) { monthWeights[m] = 0; monthWeightCounts[m] = 0; } + monthWeights[m] += w.weight; + monthWeightCounts[m]++; + }); + + // Build unified month list when weight overlay is on + const weightMonths = Object.keys(monthWeights).sort(); + const allMonths = showWeight && weights.length > 0 + ? [...new Set([...monthLabels, ...weightMonths])].sort() + : monthLabels; + + // Map spending to unified x-axis + const spendMap = {}; + monthLabels.forEach((m, i) => { spendMap[m] = monthTotals[i]; }); + const spendY = allMonths.map(m => spendMap[m] || 0); + + const traces = [{ + x: allMonths, y: spendY, + type: 'bar', name: 'Spending ($)', + marker: { color: '#7ecce5', line: { color: '#4a9bb5', width: 1.5 } }, + }]; + + const layoutExtra = {}; + + if (showWeight && weights.length > 0) { + const weightY = allMonths.map(m => monthWeights[m] ? Math.round(monthWeights[m] / monthWeightCounts[m] * 10) / 10 : null); + + traces.push({ + x: allMonths, y: weightY, + type: 'scatter', mode: 'lines+markers', name: 'Weight (lbs)', + yaxis: 'y2', + line: { color: '#d4652a', width: 3 }, + marker: { size: 7, color: '#d4652a' }, + connectgaps: true, + }); + + layoutExtra.yaxis2 = { + title: 'Weight (lbs)', + overlaying: 'y', side: 'right', + showgrid: false, + font: { color: '#d4652a' }, + titlefont: { color: '#d4652a' }, + tickfont: { color: '#d4652a' }, + }; + } + + Plotly.newPlot('monthly', traces, { + ...layout_base, + title: { text: 'Monthly Spending' + (showWeight && weights.length ? ' & Weight' : ''), font: { size: 16 } }, + ...layoutExtra, + }, cfg); + } + + document.getElementById('weight-overlay-toggle').addEventListener('change', updateMonthlyChart); + document.getElementById('weight-date').valueAsDate = new Date(); + renderWeightLog(); + updateMonthlyChart(); + Plotly.newPlot('heatmap', [{ z: {{ heatmap_data|safe }}, y: {{ heatmap_days|safe }}, type: 'heatmap',