Files
baby-monitor/src/baby_monitor/static/feedings.html
T

1179 lines
34 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Feedings - Baby Monitor</title>
<link rel="stylesheet" href="/menu.css" />
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu,
Cantarell, sans-serif;
background-color: #f0f0f0;
min-height: 100vh;
max-width: 800px;
margin: 2rem auto;
padding: 0 1rem;
}
.container {
background: white;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
padding: 40px;
}
h1 {
color: #333;
margin-bottom: 10px;
font-size: 28px;
}
.subtitle {
color: #666;
margin-bottom: 30px;
font-size: 14px;
}
.button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 12px 24px;
border-radius: 6px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition:
transform 0.2s,
box-shadow 0.2s;
text-decoration: none;
display: inline-block;
}
.button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.button.secondary {
background: #6c757d;
margin-left: 10px;
}
.button.secondary:hover {
box-shadow: 0 4px 12px rgba(108, 117, 125, 0.4);
}
.hidden {
display: none;
}
.loading {
text-align: center;
padding: 40px;
color: #666;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #666;
}
.empty-state-icon {
font-size: 64px;
margin-bottom: 20px;
}
.empty-state h2 {
color: #333;
margin-bottom: 10px;
}
.empty-state p {
margin-bottom: 30px;
}
.chart-container {
margin-bottom: 40px;
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
}
.chart-container h2 {
color: #333;
margin-bottom: 20px;
font-size: 20px;
}
.chart-wrapper {
position: relative;
height: 300px;
max-width: 100%;
margin: 0 auto;
}
.chart-wrapper canvas {
max-width: 100%;
max-height: 100%;
}
.charts-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 40px;
}
@media (max-width: 768px) {
.charts-row {
grid-template-columns: 1fr;
}
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 40px;
}
.stat-card {
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
text-align: center;
}
.stat-card h3 {
color: #666;
font-size: 14px;
font-weight: 600;
text-transform: uppercase;
margin-bottom: 10px;
}
.stat-card .stat-value {
color: #333;
font-size: 32px;
font-weight: 700;
}
.back-button {
margin-top: 20px;
}
.controls {
margin-bottom: 30px;
display: flex;
align-items: center;
gap: 15px;
flex-wrap: wrap;
}
.controls label {
font-weight: 600;
color: #333;
}
.controls select {
padding: 8px 16px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
background: white;
}
.controls select:focus {
outline: none;
border-color: #667eea;
}
.feedings-list {
margin-top: 40px;
}
.feedings-list h2 {
color: #333;
margin-bottom: 20px;
font-size: 20px;
}
.feedings-scroll {
max-height: 500px;
overflow-y: auto;
border: 1px solid #ddd;
border-radius: 8px;
background: #f8f9fa;
}
.feeding-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 20px;
background: white;
border-bottom: 1px solid #eee;
transition: background 0.2s;
}
.feeding-item:hover {
background: #f8f9fa;
}
.feeding-item:last-child {
border-bottom: none;
}
.feeding-info-left {
flex: 1;
}
.feeding-time {
font-weight: 600;
color: #333;
margin-bottom: 5px;
}
.feeding-details {
font-size: 14px;
color: #666;
}
.feeding-type-badge {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
margin-right: 8px;
}
.feeding-type-badge.left-breast {
background: rgba(102, 126, 234, 0.1);
color: rgba(102, 126, 234, 1);
}
.feeding-type-badge.right-breast {
background: rgba(118, 75, 162, 0.1);
color: rgba(118, 75, 162, 1);
}
.feeding-type-badge.bottle {
background: rgba(255, 159, 64, 0.1);
color: rgba(255, 159, 64, 1);
}
.edit-button {
background: #667eea;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
.edit-button:hover {
background: #5568d3;
}
</style>
</head>
<body>
<div class="container">
<h1>🍼 Feedings</h1>
<p class="subtitle">Overview of all feeding sessions</p>
<div id="content" class="loading">
<p>Loading...</p>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js"></script>
<script src="/menu.js"></script>
<script src="/auth-utils.js"></script>
<script>
// Check if user is authenticated
const token = getToken();
// Initialize burger menu
initBurgerMenu({ includeHome: true });
let allFeedings = [];
let allChildren = [];
let currentDaysRange =
parseInt(localStorage.getItem("lastFeedingTimeRange")) || 7;
let selectedChildId = null; // null means show first/only child
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() {
try {
const response = await fetch("/api/feedings", {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (response.ok) {
allFeedings = await response.json();
await loadChildren();
displayFeedings(allFeedings);
} else {
showError("Failed to load feedings");
}
} catch (error) {
console.error("Error:", error);
showError("Network error. Please try again.");
}
}
// Load children data
async function loadChildren() {
try {
const response = await fetch("/api/children", {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (response.ok) {
allChildren = await response.json();
// Auto-select first child if no selection exists
if (selectedChildId === null && allChildren.length > 0) {
selectedChildId = allChildren[0].id;
}
}
} catch (error) {
console.error("Error loading children:", error);
}
}
function onDaysRangeChange(event) {
currentDaysRange = parseInt(event.target.value);
localStorage.setItem("lastFeedingTimeRange", currentDaysRange);
displayFeedings(allFeedings);
}
function onChildChange(event) {
selectedChildId = parseInt(event.target.value);
localStorage.setItem("lastFeedingChildFilter", event.target.value);
displayFeedings(allFeedings);
}
function displayFeedings(feedings) {
const content = document.getElementById("content");
if (feedings.length === 0) {
content.innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">🍼</div>
<h2>No Feedings Yet</h2>
<p>Start tracking feeding sessions from the home page</p>
</div>
`;
} else {
// Prepare chart data
const barChartData = prepareBarChartData(feedings);
const pieChartData = preparePieChartData(feedings);
const durationChartData = prepareDurationChartData(feedings);
// Build child dropdown options
const childOptions = allChildren
.map(
(child) =>
`<option value="${child.id}" ${selectedChildId === child.id ? "selected" : ""}>${child.name}</option>`,
)
.join("");
content.innerHTML = `
<div class="controls">
${
allChildren.length === 1
? `
<label for="childDisplay">Child:</label>
<input
type="text"
id="childDisplay"
value="${allChildren[0].name}"
readonly
style="background-color: #f5f5f5; cursor: default; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;"
/>
`
: `
<label for="childFilter">Child:</label>
<select id="childFilter" onchange="onChildChange(event)">
${childOptions}
</select>
`
}
<label for="daysRange">Time Range:</label>
<select id="daysRange" onchange="onDaysRangeChange(event)">
<option value="1" ${currentDaysRange === 1 ? "selected" : ""}>Last 24 Hours</option>
<option value="3" ${currentDaysRange === 3 ? "selected" : ""}>Last 3 Days</option>
<option value="7" ${currentDaysRange === 7 ? "selected" : ""}>Last 7 Days</option>
<option value="14" ${currentDaysRange === 14 ? "selected" : ""}>Last 14 Days</option>
<option value="30" ${currentDaysRange === 30 ? "selected" : ""}>Last 30 Days</option>
<option value="90" ${currentDaysRange === 90 ? "selected" : ""}>Last 90 Days</option>
</select>
</div>
<div class="chart-container">
<h2>Total Feeding Time (Last ${currentDaysRange} Day${currentDaysRange !== 1 ? "s" : ""})</h2>
<div class="chart-wrapper">
<canvas id="feedingsChart"></canvas>
</div>
</div>
<div class="charts-row">
<div class="chart-container">
<h2>Feeding Type Distribution</h2>
<div class="chart-wrapper">
<canvas id="pieChart"></canvas>
</div>
</div>
<div class="chart-container">
<h2>Feeding Duration Distribution</h2>
<div class="chart-wrapper">
<canvas id="durationChart"></canvas>
</div>
</div>
</div>
`;
// Render charts
renderBarChart(barChartData);
renderPieChart(pieChartData);
renderDurationChart(durationChartData);
// Render feeding list
renderFeedingList(feedings);
}
}
function renderFeedingList(feedings) {
// Filter feedings by current time range
const now = new Date();
const daysAgo = new Date(now);
daysAgo.setDate(daysAgo.getDate() - currentDaysRange);
daysAgo.setHours(0, 0, 0, 0);
let filteredFeedings = feedings.filter((f) => {
const feedingDate = new Date(f.start_time);
return feedingDate >= daysAgo;
});
// Filter by selected child if applicable
if (selectedChildId !== null) {
filteredFeedings = filteredFeedings.filter(
(f) => f.child_id === selectedChildId,
);
}
// Sort by most recent first
filteredFeedings.sort((a, b) => {
return new Date(b.start_time) - new Date(a.start_time);
});
const listHtml = filteredFeedings
.map((feeding) => {
const startTime = new Date(feeding.start_time);
const endTime = feeding.end_time
? new Date(feeding.end_time)
: null;
const dateStr = startTime.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
const timeStr = startTime.toLocaleTimeString("en-US", {
hour: "numeric",
minute: "2-digit",
});
const duration = endTime
? Math.round((endTime - startTime) / 60000)
: null;
const typeClass = feeding.feeding_type.replace("_", "-");
const typeLabel = formatFeedingType(feeding.feeding_type);
// Get child name
const child = allChildren.find((c) => c.id === feeding.child_id);
const childName = child ? child.name : "Unknown";
return `
<div class="feeding-item">
<div class="feeding-info-left">
<div class="feeding-time">${dateStr} at ${timeStr}${selectedChildId === null ? ` - ${childName}` : ""}</div>
<div class="feeding-details">
<span class="feeding-type-badge ${typeClass}">${typeLabel}</span>
${duration !== null ? `${duration} minutes` : "Ongoing"}
</div>
</div>
<button class="edit-button" onclick="editFeeding(${feeding.id})">Edit</button>
</div>
`;
})
.join("");
const listContainer = `
<div class="feedings-list">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h2 style="margin: 0;">Feeding History</h2>
<button class="button" onclick="addManualFeeding()" style="padding: 8px 16px; font-size: 14px;"> Add Manual Entry</button>
</div>
<div class="feedings-scroll">
${listHtml || '<div class="feeding-item">No feedings in this time range</div>'}
</div>
</div>
`;
const content = document.getElementById("content");
content.insertAdjacentHTML("beforeend", listContainer);
}
function formatFeedingType(type) {
const types = {
left_breast: "Left Breast",
right_breast: "Right Breast",
bottle: "Bottle",
};
return types[type] || type;
}
function editFeeding(feedingId) {
// TODO: Implement edit functionality
// For now, redirect to log-feeding page with ID parameter
window.location.href = `/log-feeding.html?id=${feedingId}`;
}
function addManualFeeding() {
window.location.href = `/log-feeding.html`;
}
function preparePieChartData(feedings) {
const now = new Date();
const daysAgo = new Date(now);
daysAgo.setDate(daysAgo.getDate() - currentDaysRange);
daysAgo.setHours(0, 0, 0, 0);
// Filter feedings from selected time range
let recentFeedings = feedings.filter((f) => {
const feedingDate = new Date(f.start_time);
return feedingDate >= daysAgo;
});
// Filter by selected child if applicable
if (selectedChildId !== null) {
recentFeedings = recentFeedings.filter(
(f) => f.child_id === selectedChildId,
);
}
const typeCounts = {
left_breast: 0,
right_breast: 0,
bottle: 0,
};
recentFeedings.forEach((f) => {
if (typeCounts.hasOwnProperty(f.feeding_type)) {
typeCounts[f.feeding_type]++;
}
});
// Only include types with data
const labels = [];
const data = [];
const labelMap = {
left_breast: "Left Breast",
right_breast: "Right Breast",
bottle: "Bottle",
};
Object.entries(typeCounts).forEach(([type, count]) => {
if (count > 0) {
labels.push(labelMap[type]);
data.push(count);
}
});
return { labels, data };
}
function prepareDurationChartData(feedings) {
const now = new Date();
const daysAgo = new Date(now);
daysAgo.setDate(daysAgo.getDate() - currentDaysRange);
daysAgo.setHours(0, 0, 0, 0);
// Filter feedings from selected time range that have end_time
let completedFeedings = feedings.filter((f) => {
const feedingDate = new Date(f.start_time);
return feedingDate >= daysAgo && f.end_time !== null;
});
// Filter by selected child if applicable
if (selectedChildId !== null) {
completedFeedings = completedFeedings.filter(
(f) => f.child_id === selectedChildId,
);
}
// Calculate durations in minutes
const durations = completedFeedings.map((f) => {
const start = new Date(f.start_time);
const end = new Date(f.end_time);
return (end - start) / 60000; // Convert to minutes
});
// Find max duration to determine how many bins we need
const maxDuration = Math.max(...durations, 0);
const maxBin = Math.ceil(maxDuration / 5) * 5; // Round up to nearest 5
// Create 5-minute duration buckets dynamically
const buckets = {};
for (let i = 0; i < maxBin; i += 5) {
const label = `${i}-${i + 5}`;
buckets[label] = 0;
}
// Add a final bucket for anything over the max
if (maxBin > 0) {
buckets[`${maxBin}+`] = 0;
}
// Fill buckets
durations.forEach((duration) => {
const binIndex = Math.floor(duration / 5) * 5;
const label = `${binIndex}-${binIndex + 5}`;
if (buckets[label] !== undefined) {
buckets[label]++;
} else {
buckets[`${maxBin}+`]++;
}
});
return {
labels: Object.keys(buckets),
data: Object.values(buckets),
};
}
function prepareBarChartData(feedings) {
const now = new Date();
const labels = [];
const durations = []; // Array of arrays, each containing individual feeding durations
const feedingCounts = []; // For reference
// Filter by selected child if applicable
let filteredFeedings = feedings;
if (selectedChildId !== null) {
filteredFeedings = feedings.filter(
(f) => f.child_id === selectedChildId,
);
}
// Only use completed feedings
const completedFeedings = filteredFeedings.filter((f) => f.end_time);
// 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 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 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);
// 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, durations, feedingCounts };
}
function renderBarChart(chartData) {
const ctx = document.getElementById("feedingsChart").getContext("2d");
// 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: 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,
},
tooltip: {
backgroundColor: "rgba(0, 0, 0, 0.8)",
padding: 12,
titleFont: {
size: 14,
},
bodyFont: {
size: 13,
},
callbacks: {
label: function (context) {
const minutes = Math.round(context.parsed.y);
return `${context.dataset.label}: ${minutes}m`;
},
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" : ""})`;
},
},
},
},
},
});
}
function renderPieChart(chartData) {
const ctx = document.getElementById("pieChart").getContext("2d");
// Color mapping for each type
const colorMap = {
"Left Breast": {
background: "rgba(102, 126, 234, 0.8)",
border: "rgba(102, 126, 234, 1)",
},
"Right Breast": {
background: "rgba(118, 75, 162, 0.8)",
border: "rgba(118, 75, 162, 1)",
},
Bottle: {
background: "rgba(255, 159, 64, 0.8)",
border: "rgba(255, 159, 64, 1)",
},
};
// Generate colors based on labels present
const backgroundColors = chartData.labels.map(
(label) => colorMap[label]?.background || "rgba(200, 200, 200, 0.8)",
);
const borderColors = chartData.labels.map(
(label) => colorMap[label]?.border || "rgba(200, 200, 200, 1)",
);
new Chart(ctx, {
type: "pie",
data: {
labels: chartData.labels,
datasets: [
{
data: chartData.data,
backgroundColor: backgroundColors,
borderColor: borderColors,
borderWidth: 2,
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: "bottom",
labels: {
padding: 20,
font: {
size: 13,
},
},
},
tooltip: {
backgroundColor: "rgba(0, 0, 0, 0.8)",
padding: 12,
titleFont: {
size: 14,
},
bodyFont: {
size: 13,
},
callbacks: {
label: function (context) {
const count = context.parsed;
const total = context.dataset.data.reduce(
(a, b) => a + b,
0,
);
const percentage =
total > 0 ? ((count / total) * 100).toFixed(1) : 0;
return `${context.label}: ${count} (${percentage}%)`;
},
},
},
},
},
});
}
function renderDurationChart(chartData) {
const ctx = document.getElementById("durationChart").getContext("2d");
new Chart(ctx, {
type: "bar",
data: {
labels: chartData.labels,
datasets: [
{
label: "Feedings",
data: chartData.data,
backgroundColor: "rgba(76, 175, 80, 0.7)",
borderColor: "rgba(76, 175, 80, 1)",
borderWidth: 2,
borderRadius: 6,
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false,
},
tooltip: {
backgroundColor: "rgba(0, 0, 0, 0.8)",
padding: 12,
titleFont: {
size: 14,
},
bodyFont: {
size: 13,
},
callbacks: {
label: function (context) {
const count = context.parsed.y;
return `${count} feeding${count !== 1 ? "s" : ""}`;
},
},
},
},
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: {
title: {
display: true,
text: "Duration (minutes)",
font: {
size: 13,
weight: "600",
},
},
ticks: {
font: {
size: 12,
},
},
grid: {
display: false,
},
},
},
},
});
}
function showError(message) {
const content = document.getElementById("content");
content.innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">⚠️</div>
<h2>Error</h2>
<p>${message}</p>
</div>
`;
}
async function logout() {
const token = localStorage.getItem("access_token");
try {
await fetch("/api/logout", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
},
});
} catch (error) {
console.error("Logout error:", error);
} finally {
// Clear local storage and redirect
localStorage.removeItem("access_token");
localStorage.removeItem("username");
window.location.href = "/login.html";
}
}
// Load data on page load
loadFeedings();
</script>
</body>
</html>