Merge pull request 'improve overview graphs' (#22) from 24-hour-graphs-should-show-individual-hours into main
Reviewed-on: #22
This commit was merged in pull request #22.
This commit is contained in:
@@ -192,6 +192,15 @@
|
||||
Cancel
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div id="skipOption" style="text-align: center; margin-top: 20px; display: none;">
|
||||
<p style="color: #666; font-size: 14px; margin-bottom: 10px;">
|
||||
Want to accept a child share invitation first?
|
||||
</p>
|
||||
<button type="button" class="button secondary" onclick="skipToHome()" style="margin: 0;">
|
||||
Skip for now
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
@@ -205,11 +214,17 @@
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const childId = urlParams.get("id");
|
||||
const isEditMode = !!childId;
|
||||
const fromHome = urlParams.get("from") === "home"; // Check if redirected from home
|
||||
|
||||
const form = document.getElementById("addChildForm");
|
||||
const messageDiv = document.getElementById("message");
|
||||
const submitBtn = document.getElementById("submitBtn");
|
||||
|
||||
// Show skip option if user was redirected from home (has no children)
|
||||
if (fromHome && !isEditMode) {
|
||||
document.getElementById("skipOption").style.display = "block";
|
||||
}
|
||||
|
||||
// Update page title and button text if editing
|
||||
if (isEditMode) {
|
||||
document.querySelector("h1").textContent = "✏️ Edit Child";
|
||||
@@ -317,6 +332,10 @@
|
||||
function goBack() {
|
||||
window.location.href = "/";
|
||||
}
|
||||
|
||||
function skipToHome() {
|
||||
window.location.href = "/";
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -267,9 +267,15 @@
|
||||
|
||||
let allDiaperChanges = [];
|
||||
let allChildren = [];
|
||||
let currentDaysRange = 7;
|
||||
let currentDaysRange = parseInt(localStorage.getItem("lastDiaperTimeRange")) || 7;
|
||||
let selectedChildId = null; // null means "All Children"
|
||||
|
||||
// Restore last selected child from localStorage
|
||||
const lastChildId = localStorage.getItem("lastDiaperChildFilter");
|
||||
if (lastChildId && lastChildId !== "all") {
|
||||
selectedChildId = parseInt(lastChildId);
|
||||
}
|
||||
|
||||
// Load diaper changes data
|
||||
async function loadDiaperChanges() {
|
||||
try {
|
||||
@@ -311,12 +317,14 @@
|
||||
|
||||
function onDaysRangeChange(event) {
|
||||
currentDaysRange = parseInt(event.target.value);
|
||||
localStorage.setItem("lastDiaperTimeRange", currentDaysRange);
|
||||
displayDiaperChanges(allDiaperChanges);
|
||||
}
|
||||
|
||||
function onChildChange(event) {
|
||||
selectedChildId =
|
||||
event.target.value === "all" ? null : parseInt(event.target.value);
|
||||
localStorage.setItem("lastDiaperChildFilter", event.target.value);
|
||||
displayDiaperChanges(allDiaperChanges);
|
||||
}
|
||||
|
||||
@@ -365,7 +373,7 @@
|
||||
</div>
|
||||
|
||||
<div class="chart-container">
|
||||
<h2>Diaper Changes Per Day (Last ${currentDaysRange} Day${currentDaysRange !== 1 ? "s" : ""})</h2>
|
||||
<h2>Diaper Changes (Last ${currentDaysRange} Day${currentDaysRange !== 1 ? "s" : ""})</h2>
|
||||
<div class="chart-wrapper">
|
||||
<canvas id="diaperChart"></canvas>
|
||||
</div>
|
||||
@@ -627,36 +635,115 @@
|
||||
);
|
||||
}
|
||||
|
||||
// Create days based on currentDaysRange (including today)
|
||||
for (let i = currentDaysRange - 1; i >= 0; i--) {
|
||||
const date = new Date(now);
|
||||
date.setDate(date.getDate() - i);
|
||||
date.setHours(0, 0, 0, 0);
|
||||
// If 24 hours selected, show 3-hour aggregates
|
||||
if (currentDaysRange === 1) {
|
||||
// Create 8 three-hour buckets
|
||||
for (let i = 7; i >= 0; i--) {
|
||||
const periodStart = new Date(now);
|
||||
periodStart.setHours(periodStart.getHours() - (i * 3), 0, 0, 0);
|
||||
|
||||
const periodEnd = new Date(periodStart);
|
||||
periodEnd.setHours(periodEnd.getHours() + 3);
|
||||
|
||||
const dateStr = date.toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
labels.push(dateStr);
|
||||
const startLabel = periodStart.toLocaleTimeString("en-US", {
|
||||
hour: "numeric",
|
||||
hour12: true,
|
||||
});
|
||||
const endLabel = periodEnd.toLocaleTimeString("en-US", {
|
||||
hour: "numeric",
|
||||
hour12: true,
|
||||
});
|
||||
labels.push(`${startLabel}-${endLabel}`);
|
||||
|
||||
// Count poop and pee diapers for this day
|
||||
const nextDay = new Date(date);
|
||||
nextDay.setDate(nextDay.getDate() + 1);
|
||||
// Count poop and pee diapers for this 3-hour period
|
||||
const periodChanges = filteredChanges.filter((dc) => {
|
||||
const changeDate = new Date(dc.change_time);
|
||||
return changeDate >= periodStart && changeDate < periodEnd;
|
||||
});
|
||||
|
||||
const dayChanges = filteredChanges.filter((dc) => {
|
||||
const changeDate = new Date(dc.change_time);
|
||||
return changeDate >= date && changeDate < nextDay;
|
||||
});
|
||||
const poopCount = periodChanges.filter(
|
||||
(dc) => dc.poop_amount !== null,
|
||||
).length;
|
||||
const peeCount = periodChanges.filter(
|
||||
(dc) => dc.pee_amount !== null,
|
||||
).length;
|
||||
|
||||
const poopCount = dayChanges.filter(
|
||||
(dc) => dc.poop_amount !== null,
|
||||
).length;
|
||||
const peeCount = dayChanges.filter(
|
||||
(dc) => dc.pee_amount !== null,
|
||||
).length;
|
||||
poopData.push(poopCount);
|
||||
peeData.push(peeCount);
|
||||
}
|
||||
} else if (currentDaysRange === 3) {
|
||||
// Show 8-hour aggregates for 3 days (9 buckets total)
|
||||
for (let i = 8; i >= 0; i--) {
|
||||
const periodStart = new Date(now);
|
||||
periodStart.setHours(periodStart.getHours() - (i * 8), 0, 0, 0);
|
||||
|
||||
const periodEnd = new Date(periodStart);
|
||||
periodEnd.setHours(periodEnd.getHours() + 8);
|
||||
|
||||
poopData.push(poopCount);
|
||||
peeData.push(peeCount);
|
||||
// Format label based on the 8-hour period
|
||||
const dayStr = periodStart.toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
const startHour = periodStart.getHours();
|
||||
let periodLabel;
|
||||
if (startHour >= 0 && startHour < 8) {
|
||||
periodLabel = `${dayStr} Night`;
|
||||
} else if (startHour >= 8 && startHour < 16) {
|
||||
periodLabel = `${dayStr} Morning`;
|
||||
} else {
|
||||
periodLabel = `${dayStr} Evening`;
|
||||
}
|
||||
labels.push(periodLabel);
|
||||
|
||||
// Count poop and pee diapers for this 8-hour period
|
||||
const periodChanges = filteredChanges.filter((dc) => {
|
||||
const changeDate = new Date(dc.change_time);
|
||||
return changeDate >= periodStart && changeDate < periodEnd;
|
||||
});
|
||||
|
||||
const poopCount = periodChanges.filter(
|
||||
(dc) => dc.poop_amount !== null,
|
||||
).length;
|
||||
const peeCount = periodChanges.filter(
|
||||
(dc) => dc.pee_amount !== null,
|
||||
).length;
|
||||
|
||||
poopData.push(poopCount);
|
||||
peeData.push(peeCount);
|
||||
}
|
||||
} else {
|
||||
// Create days based on currentDaysRange (including today)
|
||||
for (let i = currentDaysRange - 1; i >= 0; i--) {
|
||||
const date = new Date(now);
|
||||
date.setDate(date.getDate() - i);
|
||||
date.setHours(0, 0, 0, 0);
|
||||
|
||||
const dateStr = date.toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
labels.push(dateStr);
|
||||
|
||||
// Count poop and pee diapers for this day
|
||||
const nextDay = new Date(date);
|
||||
nextDay.setDate(nextDay.getDate() + 1);
|
||||
|
||||
const dayChanges = filteredChanges.filter((dc) => {
|
||||
const changeDate = new Date(dc.change_time);
|
||||
return changeDate >= date && changeDate < nextDay;
|
||||
});
|
||||
|
||||
const poopCount = dayChanges.filter(
|
||||
(dc) => dc.poop_amount !== null,
|
||||
).length;
|
||||
const peeCount = dayChanges.filter(
|
||||
(dc) => dc.pee_amount !== null,
|
||||
).length;
|
||||
|
||||
poopData.push(poopCount);
|
||||
peeData.push(peeCount);
|
||||
}
|
||||
}
|
||||
|
||||
return { labels, poopData, peeData };
|
||||
@@ -724,7 +811,7 @@
|
||||
beginAtZero: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: "Number of Diapers",
|
||||
text: "Number of Changes",
|
||||
font: {
|
||||
size: 13,
|
||||
weight: "600",
|
||||
|
||||
@@ -304,8 +304,15 @@
|
||||
|
||||
let allFeedings = [];
|
||||
let allChildren = [];
|
||||
let currentDaysRange = 7;
|
||||
let currentDaysRange = parseInt(localStorage.getItem("lastFeedingTimeRange")) || 7;
|
||||
let selectedChildId = null; // null means "All Children"
|
||||
let barChartInstance = null;
|
||||
|
||||
// Restore last selected child from localStorage
|
||||
const lastChildId = localStorage.getItem("lastFeedingChildFilter");
|
||||
if (lastChildId && lastChildId !== "all") {
|
||||
selectedChildId = parseInt(lastChildId);
|
||||
}
|
||||
|
||||
// Load feedings data
|
||||
async function loadFeedings() {
|
||||
@@ -348,12 +355,14 @@
|
||||
|
||||
function onDaysRangeChange(event) {
|
||||
currentDaysRange = parseInt(event.target.value);
|
||||
localStorage.setItem("lastFeedingTimeRange", currentDaysRange);
|
||||
displayFeedings(allFeedings);
|
||||
}
|
||||
|
||||
function onChildChange(event) {
|
||||
selectedChildId =
|
||||
event.target.value === "all" ? null : parseInt(event.target.value);
|
||||
localStorage.setItem("lastFeedingChildFilter", event.target.value);
|
||||
displayFeedings(allFeedings);
|
||||
}
|
||||
|
||||
@@ -402,7 +411,7 @@
|
||||
</div>
|
||||
|
||||
<div class="chart-container">
|
||||
<h2>Feedings Per Day (Last ${currentDaysRange} Day${currentDaysRange !== 1 ? "s" : ""})</h2>
|
||||
<h2>Total Feeding Time (Last ${currentDaysRange} Day${currentDaysRange !== 1 ? "s" : ""})</h2>
|
||||
<div class="chart-wrapper">
|
||||
<canvas id="feedingsChart"></canvas>
|
||||
</div>
|
||||
@@ -651,7 +660,8 @@
|
||||
function prepareBarChartData(feedings) {
|
||||
const now = new Date();
|
||||
const labels = [];
|
||||
const data = [];
|
||||
const durations = []; // Array of arrays, each containing individual feeding durations
|
||||
const feedingCounts = []; // For reference
|
||||
|
||||
// Filter by selected child if applicable
|
||||
let filteredFeedings = feedings;
|
||||
@@ -661,54 +671,241 @@
|
||||
);
|
||||
}
|
||||
|
||||
// Create days based on currentDaysRange (including today)
|
||||
for (let i = currentDaysRange - 1; i >= 0; i--) {
|
||||
const date = new Date(now);
|
||||
date.setDate(date.getDate() - i);
|
||||
date.setHours(0, 0, 0, 0);
|
||||
// Only use completed feedings
|
||||
const completedFeedings = filteredFeedings.filter(f => f.end_time);
|
||||
|
||||
const dateStr = date.toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
labels.push(dateStr);
|
||||
// If 24 hours selected, show 3-hour aggregates
|
||||
if (currentDaysRange === 1) {
|
||||
// Create 8 three-hour buckets
|
||||
for (let i = 7; i >= 0; i--) {
|
||||
const periodStart = new Date(now);
|
||||
periodStart.setHours(periodStart.getHours() - (i * 3), 0, 0, 0);
|
||||
|
||||
const periodEnd = new Date(periodStart);
|
||||
periodEnd.setHours(periodEnd.getHours() + 3);
|
||||
|
||||
// Count feedings for this day
|
||||
const nextDay = new Date(date);
|
||||
nextDay.setDate(nextDay.getDate() + 1);
|
||||
const startLabel = periodStart.toLocaleTimeString("en-US", {
|
||||
hour: "numeric",
|
||||
hour12: true,
|
||||
});
|
||||
const endLabel = periodEnd.toLocaleTimeString("en-US", {
|
||||
hour: "numeric",
|
||||
hour12: true,
|
||||
});
|
||||
labels.push(`${startLabel}-${endLabel}`);
|
||||
|
||||
const count = filteredFeedings.filter((f) => {
|
||||
const feedingDate = new Date(f.start_time);
|
||||
return feedingDate >= date && feedingDate < nextDay;
|
||||
}).length;
|
||||
// Get individual feeding durations for this 3-hour period (in minutes)
|
||||
const periodFeedings = completedFeedings.filter((f) => {
|
||||
const feedingDate = new Date(f.start_time);
|
||||
return feedingDate >= periodStart && feedingDate < periodEnd;
|
||||
});
|
||||
|
||||
const feedingDurations = periodFeedings.map(feeding => {
|
||||
const start = new Date(feeding.start_time);
|
||||
const end = new Date(feeding.end_time);
|
||||
return (end - start) / 60000; // Convert to minutes
|
||||
});
|
||||
|
||||
durations.push(feedingDurations);
|
||||
feedingCounts.push(periodFeedings.length);
|
||||
}
|
||||
} else if (currentDaysRange === 3) {
|
||||
// Show 8-hour aggregates for 3 days (9 buckets total)
|
||||
for (let i = 8; i >= 0; i--) {
|
||||
const periodStart = new Date(now);
|
||||
periodStart.setHours(periodStart.getHours() - (i * 8), 0, 0, 0);
|
||||
|
||||
const periodEnd = new Date(periodStart);
|
||||
periodEnd.setHours(periodEnd.getHours() + 8);
|
||||
|
||||
data.push(count);
|
||||
// Format label based on the 8-hour period
|
||||
const dayStr = periodStart.toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
const startHour = periodStart.getHours();
|
||||
let periodLabel;
|
||||
if (startHour >= 0 && startHour < 8) {
|
||||
periodLabel = `${dayStr} Night`;
|
||||
} else if (startHour >= 8 && startHour < 16) {
|
||||
periodLabel = `${dayStr} Morning`;
|
||||
} else {
|
||||
periodLabel = `${dayStr} Evening`;
|
||||
}
|
||||
labels.push(periodLabel);
|
||||
|
||||
// Get individual feeding durations for this 8-hour period (in minutes)
|
||||
const periodFeedings = completedFeedings.filter((f) => {
|
||||
const feedingDate = new Date(f.start_time);
|
||||
return feedingDate >= periodStart && feedingDate < periodEnd;
|
||||
});
|
||||
|
||||
const feedingDurations = periodFeedings.map(feeding => {
|
||||
const start = new Date(feeding.start_time);
|
||||
const end = new Date(feeding.end_time);
|
||||
return (end - start) / 60000; // Convert to minutes
|
||||
});
|
||||
|
||||
durations.push(feedingDurations);
|
||||
feedingCounts.push(periodFeedings.length);
|
||||
}
|
||||
} else {
|
||||
// Create days based on currentDaysRange (including today)
|
||||
for (let i = currentDaysRange - 1; i >= 0; i--) {
|
||||
const date = new Date(now);
|
||||
date.setDate(date.getDate() - i);
|
||||
date.setHours(0, 0, 0, 0);
|
||||
|
||||
const dateStr = date.toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
labels.push(dateStr);
|
||||
|
||||
// Get individual feeding durations for this day (in minutes)
|
||||
const nextDay = new Date(date);
|
||||
nextDay.setDate(nextDay.getDate() + 1);
|
||||
|
||||
const dayFeedings = completedFeedings.filter((f) => {
|
||||
const feedingDate = new Date(f.start_time);
|
||||
return feedingDate >= date && feedingDate < nextDay;
|
||||
});
|
||||
|
||||
const feedingDurations = dayFeedings.map(feeding => {
|
||||
const start = new Date(feeding.start_time);
|
||||
const end = new Date(feeding.end_time);
|
||||
return (end - start) / 60000; // Convert to minutes
|
||||
});
|
||||
|
||||
durations.push(feedingDurations);
|
||||
feedingCounts.push(dayFeedings.length);
|
||||
}
|
||||
}
|
||||
|
||||
return { labels, data };
|
||||
return { labels, durations, feedingCounts };
|
||||
}
|
||||
|
||||
function renderBarChart(chartData) {
|
||||
const ctx = document.getElementById("feedingsChart").getContext("2d");
|
||||
|
||||
new Chart(ctx, {
|
||||
// Destroy existing chart if it exists
|
||||
if (barChartInstance) {
|
||||
barChartInstance.destroy();
|
||||
}
|
||||
|
||||
// Find the maximum number of feedings in any period
|
||||
const maxFeedings = Math.max(...chartData.durations.map(d => d.length), 0);
|
||||
|
||||
// If no feedings at all, show empty chart
|
||||
if (maxFeedings === 0) {
|
||||
barChartInstance = new Chart(ctx, {
|
||||
type: "bar",
|
||||
data: {
|
||||
labels: chartData.labels,
|
||||
datasets: [{
|
||||
label: "No Data",
|
||||
data: new Array(chartData.labels.length).fill(0),
|
||||
backgroundColor: "rgba(102, 126, 234, 0.3)",
|
||||
}],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
max: 60,
|
||||
title: {
|
||||
display: true,
|
||||
text: "Minutes",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Create color palette for different feedings
|
||||
const colors = [
|
||||
'rgba(102, 126, 234, 0.8)',
|
||||
'rgba(118, 75, 162, 0.8)',
|
||||
'rgba(255, 159, 64, 0.8)',
|
||||
'rgba(76, 175, 80, 0.8)',
|
||||
'rgba(244, 67, 54, 0.8)',
|
||||
'rgba(156, 39, 176, 0.8)',
|
||||
'rgba(102, 126, 234, 0.6)',
|
||||
'rgba(118, 75, 162, 0.6)',
|
||||
'rgba(255, 159, 64, 0.6)',
|
||||
'rgba(76, 175, 80, 0.6)',
|
||||
];
|
||||
|
||||
// Create datasets - one for each feeding position
|
||||
const datasets = [];
|
||||
for (let feedingIndex = 0; feedingIndex < maxFeedings; feedingIndex++) {
|
||||
const dataForFeeding = chartData.durations.map(periodDurations =>
|
||||
periodDurations[feedingIndex] || 0
|
||||
);
|
||||
|
||||
datasets.push({
|
||||
label: `Feeding ${feedingIndex + 1}`,
|
||||
data: dataForFeeding,
|
||||
backgroundColor: colors[feedingIndex % colors.length],
|
||||
borderColor: colors[feedingIndex % colors.length].replace('0.8', '1').replace('0.6', '1'),
|
||||
borderWidth: 1,
|
||||
});
|
||||
}
|
||||
|
||||
barChartInstance = new Chart(ctx, {
|
||||
type: "bar",
|
||||
data: {
|
||||
labels: chartData.labels,
|
||||
datasets: [
|
||||
{
|
||||
label: "Feedings",
|
||||
data: chartData.data,
|
||||
backgroundColor: "rgba(102, 126, 234, 0.7)",
|
||||
borderColor: "rgba(102, 126, 234, 1)",
|
||||
borderWidth: 2,
|
||||
borderRadius: 6,
|
||||
},
|
||||
],
|
||||
datasets: datasets,
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
x: {
|
||||
stacked: true,
|
||||
ticks: {
|
||||
font: {
|
||||
size: 12,
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
y: {
|
||||
stacked: true,
|
||||
beginAtZero: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: "Minutes",
|
||||
font: {
|
||||
size: 13,
|
||||
weight: "600",
|
||||
},
|
||||
},
|
||||
ticks: {
|
||||
font: {
|
||||
size: 12,
|
||||
},
|
||||
callback: function(value) {
|
||||
return Math.round(value) + 'm';
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
color: "rgba(0, 0, 0, 0.05)",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
@@ -723,43 +920,17 @@
|
||||
size: 13,
|
||||
},
|
||||
callbacks: {
|
||||
label: function (context) {
|
||||
const count = context.parsed.y;
|
||||
return `${count} feeding${count !== 1 ? "s" : ""}`;
|
||||
label: function(context) {
|
||||
const minutes = Math.round(context.parsed.y);
|
||||
return `${context.dataset.label}: ${minutes}m`;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: "Number of Feedings",
|
||||
font: {
|
||||
size: 13,
|
||||
weight: "600",
|
||||
},
|
||||
},
|
||||
ticks: {
|
||||
stepSize: 1,
|
||||
font: {
|
||||
size: 12,
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
color: "rgba(0, 0, 0, 0.05)",
|
||||
},
|
||||
},
|
||||
x: {
|
||||
ticks: {
|
||||
font: {
|
||||
size: 12,
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
display: false,
|
||||
},
|
||||
footer: function(tooltipItems) {
|
||||
const periodIndex = tooltipItems[0].dataIndex;
|
||||
const totalMinutes = chartData.durations[periodIndex].reduce((sum, val) => sum + val, 0);
|
||||
const feedingCount = chartData.feedingCounts[periodIndex];
|
||||
return `Total: ${Math.round(totalMinutes)}m (${feedingCount} feeding${feedingCount !== 1 ? 's' : ''})`;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -275,9 +275,9 @@
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((children) => {
|
||||
// If no children, redirect to add-child page
|
||||
// If no children, show welcome message with option to add
|
||||
if (children.length === 0) {
|
||||
window.location.href = "/add-child.html";
|
||||
showNoChildrenMessage();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -290,6 +290,30 @@
|
||||
});
|
||||
}
|
||||
|
||||
function showNoChildrenMessage() {
|
||||
const content = document.getElementById("content");
|
||||
content.className = "";
|
||||
content.innerHTML = `
|
||||
<h1>👶 Welcome to Baby Monitor</h1>
|
||||
<div style="text-align: center; padding: 40px 20px;">
|
||||
<div style="font-size: 64px; margin-bottom: 20px;">🍼</div>
|
||||
<h2 style="color: #333; margin-bottom: 15px;">No Children Added Yet</h2>
|
||||
<p style="color: #666; margin-bottom: 30px; line-height: 1.6;">
|
||||
You can add a child to start tracking or redeem a child share code<br>
|
||||
from another user to access their child's information.
|
||||
</p>
|
||||
<div style="display: flex; gap: 15px; justify-content: center; flex-wrap: wrap;">
|
||||
<a href="/add-child.html" class="button" style="text-decoration: none; display: inline-block;">
|
||||
➕ Add Your Child
|
||||
</a>
|
||||
<a href="/settings.html" class="button secondary" style="text-decoration: none; display: inline-block;">
|
||||
🔗 Redeem Share Code
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
let activeFeeding = null;
|
||||
let userChildren = [];
|
||||
let lastFeeding = null;
|
||||
|
||||
@@ -231,33 +231,31 @@
|
||||
background: #3a9be8;
|
||||
}
|
||||
|
||||
.filters {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
.controls {
|
||||
margin-bottom: 30px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-group label {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
.controls label {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.filter-group select {
|
||||
padding: 8px 12px;
|
||||
.controls select {
|
||||
padding: 8px 16px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.controls select:focus {
|
||||
outline: none;
|
||||
border-color: #4facfe;
|
||||
}
|
||||
|
||||
.badge {
|
||||
@@ -280,24 +278,21 @@
|
||||
<h1>😴 Sleep Tracking</h1>
|
||||
<p class="subtitle">Monitor your baby's sleep patterns</p>
|
||||
|
||||
<div class="filters">
|
||||
<div class="filter-group">
|
||||
<label for="childFilter">Child</label>
|
||||
<select id="childFilter">
|
||||
<option value="">All Children</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label for="timeRangeFilter">Time Range</label>
|
||||
<select id="timeRangeFilter">
|
||||
<option value="1">Last 24 hours</option>
|
||||
<option value="3">Last 3 days</option>
|
||||
<option value="7" selected>Last 7 days</option>
|
||||
<option value="14">Last 14 days</option>
|
||||
<option value="30">Last 30 days</option>
|
||||
<option value="90">Last 90 days</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<label for="childFilter">Child:</label>
|
||||
<select id="childFilter">
|
||||
<option value="">All Children</option>
|
||||
</select>
|
||||
|
||||
<label for="timeRangeFilter">Time Range:</label>
|
||||
<select id="timeRangeFilter">
|
||||
<option value="1">Last 24 hours</option>
|
||||
<option value="3">Last 3 days</option>
|
||||
<option value="7" selected>Last 7 days</option>
|
||||
<option value="14">Last 14 days</option>
|
||||
<option value="30">Last 30 days</option>
|
||||
<option value="90">Last 90 days</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="content" class="loading">
|
||||
@@ -339,6 +334,8 @@
|
||||
document
|
||||
.getElementById("timeRangeFilter")
|
||||
.addEventListener("change", function () {
|
||||
// Save selection to localStorage
|
||||
localStorage.setItem("lastSleepTimeRange", this.value);
|
||||
loadData();
|
||||
});
|
||||
|
||||
@@ -374,6 +371,13 @@
|
||||
selectedChildId = lastChildId;
|
||||
}
|
||||
|
||||
// Restore last selected time range from localStorage
|
||||
const timeRangeFilter = document.getElementById("timeRangeFilter");
|
||||
const lastTimeRange = localStorage.getItem("lastSleepTimeRange");
|
||||
if (lastTimeRange) {
|
||||
timeRangeFilter.value = lastTimeRange;
|
||||
}
|
||||
|
||||
// Fetch sleep logs
|
||||
const sleepResponse = await fetch("/api/sleep", {
|
||||
headers: {
|
||||
@@ -480,7 +484,7 @@
|
||||
</div>
|
||||
|
||||
<div class="chart-container">
|
||||
<h2>Sleep Sessions per Day</h2>
|
||||
<h2>Total Sleep Time (Last ${timeRange} Day${timeRange !== 1 ? "s" : ""})</h2>
|
||||
<div class="chart-wrapper">
|
||||
<canvas id="barChart"></canvas>
|
||||
</div>
|
||||
@@ -536,41 +540,131 @@
|
||||
return `${hours}h ${mins}m`;
|
||||
}
|
||||
|
||||
function prepareBarChartData(sleeps, timeRange) {
|
||||
const days = {};
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
function prepareBarChartData(sleepSessions, timeRange) {
|
||||
const now = new Date();
|
||||
const labels = [];
|
||||
const durations = []; // Array of arrays, each containing individual session durations
|
||||
const sessionCounts = []; // For reference
|
||||
|
||||
// Initialize all days in range
|
||||
for (let i = timeRange - 1; i >= 0; i--) {
|
||||
const date = new Date(today);
|
||||
date.setDate(date.getDate() - i);
|
||||
const dateStr = date.toISOString().split("T")[0];
|
||||
days[dateStr] = 0;
|
||||
// Only use completed sleep sessions
|
||||
const completedSessions = sleepSessions.filter(s => s.end_time);
|
||||
|
||||
// If 24 hours selected, show 3-hour aggregates
|
||||
if (timeRange === 1) {
|
||||
// Create 8 three-hour buckets
|
||||
for (let i = 7; i >= 0; i--) {
|
||||
const periodStart = new Date(now);
|
||||
periodStart.setHours(periodStart.getHours() - (i * 3), 0, 0, 0);
|
||||
|
||||
const periodEnd = new Date(periodStart);
|
||||
periodEnd.setHours(periodEnd.getHours() + 3);
|
||||
|
||||
const startLabel = periodStart.toLocaleTimeString("en-US", {
|
||||
hour: "numeric",
|
||||
hour12: true,
|
||||
});
|
||||
const endLabel = periodEnd.toLocaleTimeString("en-US", {
|
||||
hour: "numeric",
|
||||
hour12: true,
|
||||
});
|
||||
labels.push(`${startLabel}-${endLabel}`);
|
||||
|
||||
// Get individual session durations for this 3-hour period (in hours)
|
||||
const periodSessions = completedSessions.filter((session) => {
|
||||
const startDate = new Date(session.start_time);
|
||||
return startDate >= periodStart && startDate < periodEnd;
|
||||
});
|
||||
|
||||
const sessionDurations = periodSessions.map(session =>
|
||||
calculateDuration(session.start_time, session.end_time) / 60
|
||||
);
|
||||
|
||||
durations.push(sessionDurations);
|
||||
sessionCounts.push(periodSessions.length);
|
||||
}
|
||||
} else if (timeRange === 3) {
|
||||
// Show 8-hour aggregates for 3 days (9 buckets total)
|
||||
for (let i = 8; i >= 0; i--) {
|
||||
const periodStart = new Date(now);
|
||||
periodStart.setHours(periodStart.getHours() - (i * 8), 0, 0, 0);
|
||||
|
||||
const periodEnd = new Date(periodStart);
|
||||
periodEnd.setHours(periodEnd.getHours() + 8);
|
||||
|
||||
// Format label based on the 8-hour period
|
||||
const dayStr = periodStart.toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
const startHour = periodStart.getHours();
|
||||
let periodLabel;
|
||||
if (startHour >= 0 && startHour < 8) {
|
||||
periodLabel = `${dayStr} Night`;
|
||||
} else if (startHour >= 8 && startHour < 16) {
|
||||
periodLabel = `${dayStr} Morning`;
|
||||
} else {
|
||||
periodLabel = `${dayStr} Evening`;
|
||||
}
|
||||
labels.push(periodLabel);
|
||||
|
||||
// Get individual session durations for this 8-hour period (in hours)
|
||||
const periodSessions = completedSessions.filter((session) => {
|
||||
const startDate = new Date(session.start_time);
|
||||
return startDate >= periodStart && startDate < periodEnd;
|
||||
});
|
||||
|
||||
const sessionDurations = periodSessions.map(session =>
|
||||
calculateDuration(session.start_time, session.end_time) / 60
|
||||
);
|
||||
|
||||
durations.push(sessionDurations);
|
||||
sessionCounts.push(periodSessions.length);
|
||||
}
|
||||
} else {
|
||||
// Create days based on timeRange (including today)
|
||||
for (let i = timeRange - 1; i >= 0; i--) {
|
||||
const date = new Date(now);
|
||||
date.setDate(date.getDate() - i);
|
||||
date.setHours(0, 0, 0, 0);
|
||||
|
||||
const dateStr = date.toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
labels.push(dateStr);
|
||||
|
||||
// Get individual session durations for this day (in hours)
|
||||
const nextDay = new Date(date);
|
||||
nextDay.setDate(nextDay.getDate() + 1);
|
||||
|
||||
const daySessions = completedSessions.filter((session) => {
|
||||
const startDate = new Date(session.start_time);
|
||||
return startDate >= date && startDate < nextDay;
|
||||
});
|
||||
|
||||
const sessionDurations = daySessions.map(session => {
|
||||
const dur = calculateDuration(session.start_time, session.end_time) / 60;
|
||||
console.log(` Session: ${session.start_time} to ${session.end_time}, duration: ${dur} hours (${calculateDuration(session.start_time, session.end_time)} minutes)`);
|
||||
return dur;
|
||||
});
|
||||
|
||||
console.log(`${dateStr}: ${daySessions.length} sessions, durations:`, sessionDurations);
|
||||
|
||||
durations.push(sessionDurations);
|
||||
sessionCounts.push(daySessions.length);
|
||||
}
|
||||
}
|
||||
|
||||
// Count sleeps per day
|
||||
sleeps.forEach((sleep) => {
|
||||
const date = new Date(sleep.start_time);
|
||||
date.setHours(0, 0, 0, 0);
|
||||
const dateStr = date.toISOString().split("T")[0];
|
||||
if (days.hasOwnProperty(dateStr)) {
|
||||
days[dateStr]++;
|
||||
console.log('Final prepareBarChartData result:', { labels, durations, sessionCounts });
|
||||
|
||||
// Log the actual duration values for debugging
|
||||
durations.forEach((periodDurations, idx) => {
|
||||
if (periodDurations.length > 0) {
|
||||
console.log(` ${labels[idx]}: durations =`, periodDurations);
|
||||
}
|
||||
});
|
||||
|
||||
const labels = Object.keys(days).map((date) => {
|
||||
const d = new Date(date);
|
||||
return d.toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
labels,
|
||||
data: Object.values(days),
|
||||
};
|
||||
|
||||
return { labels, durations, sessionCounts };
|
||||
}
|
||||
|
||||
function prepareDurationDistributionData(sleeps) {
|
||||
@@ -599,30 +693,139 @@
|
||||
};
|
||||
}
|
||||
|
||||
let barChartInstance = null;
|
||||
let durationChartInstance = null;
|
||||
let avgDurationChartInstance = null;
|
||||
|
||||
function renderBarChart(chartData) {
|
||||
const ctx = document.getElementById("barChart");
|
||||
new Chart(ctx, {
|
||||
|
||||
// Destroy existing chart if it exists
|
||||
if (barChartInstance) {
|
||||
barChartInstance.destroy();
|
||||
}
|
||||
|
||||
// Debug: Log the data
|
||||
console.log('Bar chart data:', chartData);
|
||||
|
||||
// Find the maximum number of sessions in any period
|
||||
const maxSessions = Math.max(...chartData.durations.map(d => d.length), 0);
|
||||
|
||||
console.log('Max sessions:', maxSessions);
|
||||
|
||||
// If no sessions at all, show empty chart
|
||||
if (maxSessions === 0) {
|
||||
barChartInstance = new Chart(ctx, {
|
||||
type: "bar",
|
||||
data: {
|
||||
labels: chartData.labels,
|
||||
datasets: [{
|
||||
label: "No Data",
|
||||
data: new Array(chartData.labels.length).fill(0),
|
||||
backgroundColor: "rgba(79, 172, 254, 0.3)",
|
||||
}],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
max: 10,
|
||||
title: {
|
||||
display: true,
|
||||
text: "Hours",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Create color palette for different sessions
|
||||
const colors = [
|
||||
'rgba(79, 172, 254, 0.8)',
|
||||
'rgba(0, 242, 254, 0.8)',
|
||||
'rgba(58, 155, 232, 0.8)',
|
||||
'rgba(32, 137, 220, 0.8)',
|
||||
'rgba(21, 119, 208, 0.8)',
|
||||
'rgba(11, 101, 196, 0.8)',
|
||||
'rgba(79, 172, 254, 0.6)',
|
||||
'rgba(0, 242, 254, 0.6)',
|
||||
'rgba(58, 155, 232, 0.6)',
|
||||
'rgba(32, 137, 220, 0.6)',
|
||||
];
|
||||
|
||||
// Create datasets - one for each session position
|
||||
const datasets = [];
|
||||
for (let sessionIndex = 0; sessionIndex < maxSessions; sessionIndex++) {
|
||||
const dataForSession = chartData.durations.map(periodDurations =>
|
||||
periodDurations[sessionIndex] || 0
|
||||
);
|
||||
|
||||
console.log(`Session ${sessionIndex + 1} data:`, dataForSession);
|
||||
|
||||
datasets.push({
|
||||
label: `Session ${sessionIndex + 1}`,
|
||||
data: dataForSession,
|
||||
backgroundColor: colors[sessionIndex % colors.length],
|
||||
borderColor: colors[sessionIndex % colors.length].replace('0.8', '1').replace('0.6', '1'),
|
||||
borderWidth: 1,
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Datasets:', datasets);
|
||||
|
||||
// Determine the maximum total duration across all periods (in hours)
|
||||
const maxDuration = Math.max(...chartData.durations.map(periodDurations =>
|
||||
periodDurations.reduce((sum, val) => sum + val, 0)
|
||||
), 0);
|
||||
|
||||
console.log('Max duration (hours):', maxDuration);
|
||||
|
||||
// Decide whether to use minutes or hours based on max duration
|
||||
const useMinutes = maxDuration < 2; // Use minutes if max is less than 2 hours
|
||||
|
||||
// Convert data to minutes if needed
|
||||
const finalDatasets = useMinutes ? datasets.map(dataset => ({
|
||||
...dataset,
|
||||
data: dataset.data.map(hours => hours * 60) // Convert hours to minutes
|
||||
})) : datasets;
|
||||
|
||||
barChartInstance = new Chart(ctx, {
|
||||
type: "bar",
|
||||
data: {
|
||||
labels: chartData.labels,
|
||||
datasets: [
|
||||
{
|
||||
label: "Sleep Sessions",
|
||||
data: chartData.data,
|
||||
backgroundColor: "rgba(79, 172, 254, 0.6)",
|
||||
borderColor: "rgba(79, 172, 254, 1)",
|
||||
borderWidth: 1,
|
||||
},
|
||||
],
|
||||
datasets: finalDatasets,
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
x: {
|
||||
stacked: true,
|
||||
},
|
||||
y: {
|
||||
stacked: true,
|
||||
beginAtZero: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: useMinutes ? "Minutes" : "Hours",
|
||||
},
|
||||
ticks: {
|
||||
stepSize: 1,
|
||||
callback: function(value) {
|
||||
if (useMinutes) {
|
||||
return Math.round(value) + 'm';
|
||||
} else {
|
||||
return value.toFixed(1) + 'h';
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -630,6 +833,30 @@
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
const value = useMinutes ? context.parsed.y / 60 : context.parsed.y;
|
||||
const hours = Math.floor(value);
|
||||
const minutes = Math.round((value - hours) * 60);
|
||||
if (hours === 0) {
|
||||
return `${context.dataset.label}: ${minutes}m`;
|
||||
}
|
||||
return `${context.dataset.label}: ${hours}h ${minutes}m`;
|
||||
},
|
||||
footer: function(tooltipItems) {
|
||||
const periodIndex = tooltipItems[0].dataIndex;
|
||||
const totalHours = chartData.durations[periodIndex].reduce((sum, val) => sum + val, 0);
|
||||
const hours = Math.floor(totalHours);
|
||||
const minutes = Math.round((totalHours - hours) * 60);
|
||||
const sessionCount = chartData.sessionCounts[periodIndex];
|
||||
if (hours === 0) {
|
||||
return `Total: ${minutes}m (${sessionCount} session${sessionCount !== 1 ? 's' : ''})`;
|
||||
}
|
||||
return `Total: ${hours}h ${minutes}m (${sessionCount} session${sessionCount !== 1 ? 's' : ''})`;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -637,7 +864,13 @@
|
||||
|
||||
function renderDurationChart(chartData) {
|
||||
const ctx = document.getElementById("durationChart");
|
||||
new Chart(ctx, {
|
||||
|
||||
// Destroy existing chart if it exists
|
||||
if (durationChartInstance) {
|
||||
durationChartInstance.destroy();
|
||||
}
|
||||
|
||||
durationChartInstance = new Chart(ctx, {
|
||||
type: "pie",
|
||||
data: {
|
||||
labels: chartData.labels,
|
||||
@@ -668,6 +901,11 @@
|
||||
}
|
||||
|
||||
function renderAvgDurationChart(sleeps, timeRange) {
|
||||
// Destroy existing chart if it exists
|
||||
if (avgDurationChartInstance) {
|
||||
avgDurationChartInstance.destroy();
|
||||
}
|
||||
|
||||
const days = {};
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
@@ -710,7 +948,7 @@
|
||||
);
|
||||
|
||||
const ctx = document.getElementById("avgDurationChart");
|
||||
new Chart(ctx, {
|
||||
avgDurationChartInstance = new Chart(ctx, {
|
||||
type: "line",
|
||||
data: {
|
||||
labels,
|
||||
|
||||
Reference in New Issue
Block a user