1093 lines
31 KiB
HTML
1093 lines
31 KiB
HTML
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>Diapers - 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, #f093fb 0%, #f5576c 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(240, 147, 251, 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;
|
||
}
|
||
}
|
||
|
||
.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: #f093fb;
|
||
}
|
||
|
||
.diapers-list {
|
||
margin-top: 40px;
|
||
}
|
||
|
||
.diapers-list h2 {
|
||
color: #333;
|
||
margin-bottom: 20px;
|
||
font-size: 20px;
|
||
}
|
||
|
||
.diapers-scroll {
|
||
max-height: 500px;
|
||
overflow-y: auto;
|
||
border: 1px solid #ddd;
|
||
border-radius: 8px;
|
||
background: #f8f9fa;
|
||
}
|
||
|
||
.diaper-item {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 15px 20px;
|
||
background: white;
|
||
border-bottom: 1px solid #eee;
|
||
transition: background 0.2s;
|
||
}
|
||
|
||
.diaper-item:hover {
|
||
background: #f8f9fa;
|
||
}
|
||
|
||
.diaper-item:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.diaper-info-left {
|
||
flex: 1;
|
||
}
|
||
|
||
.diaper-time {
|
||
font-weight: 600;
|
||
color: #333;
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
.diaper-details {
|
||
font-size: 14px;
|
||
color: #666;
|
||
}
|
||
|
||
.diaper-badge {
|
||
display: inline-block;
|
||
padding: 4px 8px;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
margin-right: 8px;
|
||
}
|
||
|
||
.diaper-badge.poop {
|
||
background: rgba(139, 69, 19, 0.1);
|
||
color: rgba(139, 69, 19, 1);
|
||
}
|
||
|
||
.diaper-badge.pee {
|
||
background: rgba(255, 215, 0, 0.2);
|
||
color: rgba(184, 134, 11, 1);
|
||
}
|
||
|
||
.edit-button {
|
||
background: #f093fb;
|
||
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: #d77ee6;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<h1>🧷 Diaper Changes</h1>
|
||
<p class="subtitle">Overview of all diaper change 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>
|
||
// Check if user is authenticated
|
||
const token = localStorage.getItem("access_token");
|
||
if (!token) {
|
||
window.location.href = "/login.html";
|
||
}
|
||
|
||
// Initialize burger menu
|
||
initBurgerMenu({ includeHome: true });
|
||
|
||
let allDiaperChanges = [];
|
||
let allChildren = [];
|
||
let currentDaysRange =
|
||
parseInt(localStorage.getItem("lastDiaperTimeRange")) || 7;
|
||
let selectedChildId = null; // null means show first/only child
|
||
let barChartInstance = null; // Track chart instance for proper cleanup
|
||
let poopPieChartInstance = null;
|
||
let peePieChartInstance = null;
|
||
|
||
// 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 {
|
||
const response = await fetch("/api/diaper-changes", {
|
||
headers: {
|
||
Authorization: `Bearer ${token}`,
|
||
},
|
||
});
|
||
|
||
if (response.ok) {
|
||
allDiaperChanges = await response.json();
|
||
await loadChildren();
|
||
displayDiaperChanges(allDiaperChanges);
|
||
} else {
|
||
showError("Failed to load diaper changes");
|
||
}
|
||
} 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("lastDiaperTimeRange", currentDaysRange);
|
||
displayDiaperChanges(allDiaperChanges);
|
||
}
|
||
|
||
function onChildChange(event) {
|
||
selectedChildId = parseInt(event.target.value);
|
||
localStorage.setItem("lastDiaperChildFilter", event.target.value);
|
||
displayDiaperChanges(allDiaperChanges);
|
||
}
|
||
|
||
function displayDiaperChanges(diaperChanges) {
|
||
const content = document.getElementById("content");
|
||
|
||
if (diaperChanges.length === 0) {
|
||
content.innerHTML = `
|
||
<div class="empty-state">
|
||
<div class="empty-state-icon">🧷</div>
|
||
<h2>No Diaper Changes Yet</h2>
|
||
<p>Start tracking diaper changes from the home page</p>
|
||
</div>
|
||
`;
|
||
} else {
|
||
// Prepare chart data
|
||
const barChartData = prepareBarChartData(diaperChanges);
|
||
const poopPieData = preparePoopPieChartData(diaperChanges);
|
||
const peePieData = preparePeePieChartData(diaperChanges);
|
||
|
||
// 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>Diaper Changes (Last ${currentDaysRange} Day${currentDaysRange !== 1 ? "s" : ""})</h2>
|
||
<div class="chart-wrapper">
|
||
<canvas id="diaperChart"></canvas>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="charts-row">
|
||
<div class="chart-container">
|
||
<h2>💩 Poop Distribution</h2>
|
||
<div class="chart-wrapper">
|
||
<canvas id="poopPieChart"></canvas>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="chart-container">
|
||
<h2>💧 Pee Distribution</h2>
|
||
<div class="chart-wrapper">
|
||
<canvas id="peePieChart"></canvas>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
// Render charts
|
||
renderBarChart(barChartData);
|
||
renderPoopPieChart(poopPieData);
|
||
renderPeePieChart(peePieData);
|
||
|
||
// Render diaper change list
|
||
renderDiaperList(diaperChanges);
|
||
}
|
||
}
|
||
|
||
function renderDiaperList(diaperChanges) {
|
||
// Filter 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 filteredChanges = diaperChanges.filter((dc) => {
|
||
const changeDate = new Date(dc.change_time);
|
||
return changeDate >= daysAgo;
|
||
});
|
||
|
||
// Filter by selected child if applicable
|
||
if (selectedChildId !== null) {
|
||
filteredChanges = filteredChanges.filter(
|
||
(dc) => dc.child_id === selectedChildId,
|
||
);
|
||
}
|
||
|
||
// Sort by most recent first
|
||
filteredChanges.sort((a, b) => {
|
||
return new Date(b.change_time) - new Date(a.change_time);
|
||
});
|
||
|
||
const listHtml = filteredChanges
|
||
.map((dc) => {
|
||
const changeTime = new Date(dc.change_time);
|
||
|
||
const dateStr = changeTime.toLocaleDateString("en-US", {
|
||
month: "short",
|
||
day: "numeric",
|
||
year: "numeric",
|
||
});
|
||
|
||
const timeStr = changeTime.toLocaleTimeString("en-US", {
|
||
hour: "numeric",
|
||
minute: "2-digit",
|
||
});
|
||
|
||
// Build badges for poop and pee
|
||
let badges = [];
|
||
if (dc.poop_amount || dc.poop_color) {
|
||
const poopInfo = [dc.poop_amount, dc.poop_color]
|
||
.filter(Boolean)
|
||
.join(", ");
|
||
badges.push(
|
||
`<span class="diaper-badge poop">💩 ${poopInfo}</span>`,
|
||
);
|
||
}
|
||
if (dc.pee_amount || dc.pee_color) {
|
||
const peeInfo = [dc.pee_amount, dc.pee_color]
|
||
.filter(Boolean)
|
||
.join(", ");
|
||
badges.push(
|
||
`<span class="diaper-badge pee">💧 ${peeInfo}</span>`,
|
||
);
|
||
}
|
||
if (badges.length === 0) {
|
||
badges.push(
|
||
'<span class="diaper-badge" style="background: #f0f0f0; color: #666;">Clean diaper</span>',
|
||
);
|
||
}
|
||
|
||
// Get child name
|
||
const child = allChildren.find((c) => c.id === dc.child_id);
|
||
const childName = child ? child.name : "Unknown";
|
||
|
||
return `
|
||
<div class="diaper-item">
|
||
<div class="diaper-info-left">
|
||
<div class="diaper-time">${dateStr} at ${timeStr}${selectedChildId === null ? ` - ${childName}` : ""}</div>
|
||
<div class="diaper-details">
|
||
${badges.join("")}
|
||
</div>
|
||
</div>
|
||
<button class="edit-button" onclick="editDiaperChange(${dc.id})">Edit</button>
|
||
</div>
|
||
`;
|
||
})
|
||
.join("");
|
||
|
||
const listContainer = `
|
||
<div class="diapers-list">
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
||
<h2 style="margin: 0;">Diaper Change History</h2>
|
||
<button class="button" onclick="addManualDiaperChange()" style="padding: 8px 16px; font-size: 14px;">➕ Add Manual Entry</button>
|
||
</div>
|
||
<div class="diapers-scroll">
|
||
${listHtml || '<div class="diaper-item">No diaper changes in this time range</div>'}
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
const content = document.getElementById("content");
|
||
content.insertAdjacentHTML("beforeend", listContainer);
|
||
}
|
||
|
||
function editDiaperChange(diaperChangeId) {
|
||
window.location.href = `/log-diaper.html?id=${diaperChangeId}`;
|
||
}
|
||
|
||
function addManualDiaperChange() {
|
||
window.location.href = `/log-diaper.html`;
|
||
}
|
||
|
||
function preparePoopPieChartData(diaperChanges) {
|
||
const now = new Date();
|
||
const daysAgo = new Date(now);
|
||
daysAgo.setDate(daysAgo.getDate() - currentDaysRange);
|
||
daysAgo.setHours(0, 0, 0, 0);
|
||
|
||
// Filter by time range
|
||
let recentChanges = diaperChanges.filter((dc) => {
|
||
const changeDate = new Date(dc.change_time);
|
||
return changeDate >= daysAgo;
|
||
});
|
||
|
||
// Filter by selected child if applicable
|
||
if (selectedChildId !== null) {
|
||
recentChanges = recentChanges.filter(
|
||
(dc) => dc.child_id === selectedChildId,
|
||
);
|
||
}
|
||
|
||
// Count poop amounts
|
||
const counts = {
|
||
light: 0,
|
||
medium: 0,
|
||
heavy: 0,
|
||
none: 0,
|
||
};
|
||
|
||
recentChanges.forEach((dc) => {
|
||
if (dc.poop_amount) {
|
||
counts[dc.poop_amount]++;
|
||
} else {
|
||
counts.none++;
|
||
}
|
||
});
|
||
|
||
// Only include categories with data
|
||
const labels = [];
|
||
const data = [];
|
||
const labelMap = {
|
||
light: "Light",
|
||
medium: "Medium",
|
||
heavy: "Heavy",
|
||
none: "No Poop",
|
||
};
|
||
|
||
Object.entries(counts).forEach(([type, count]) => {
|
||
if (count > 0) {
|
||
labels.push(labelMap[type]);
|
||
data.push(count);
|
||
}
|
||
});
|
||
|
||
return { labels, data };
|
||
}
|
||
|
||
function preparePeePieChartData(diaperChanges) {
|
||
const now = new Date();
|
||
const daysAgo = new Date(now);
|
||
daysAgo.setDate(daysAgo.getDate() - currentDaysRange);
|
||
daysAgo.setHours(0, 0, 0, 0);
|
||
|
||
// Filter by time range
|
||
let recentChanges = diaperChanges.filter((dc) => {
|
||
const changeDate = new Date(dc.change_time);
|
||
return changeDate >= daysAgo;
|
||
});
|
||
|
||
// Filter by selected child if applicable
|
||
if (selectedChildId !== null) {
|
||
recentChanges = recentChanges.filter(
|
||
(dc) => dc.child_id === selectedChildId,
|
||
);
|
||
}
|
||
|
||
// Count pee amounts
|
||
const counts = {
|
||
light: 0,
|
||
medium: 0,
|
||
heavy: 0,
|
||
none: 0,
|
||
};
|
||
|
||
recentChanges.forEach((dc) => {
|
||
if (dc.pee_amount) {
|
||
counts[dc.pee_amount]++;
|
||
} else {
|
||
counts.none++;
|
||
}
|
||
});
|
||
|
||
// Only include categories with data
|
||
const labels = [];
|
||
const data = [];
|
||
const labelMap = {
|
||
light: "Light",
|
||
medium: "Medium",
|
||
heavy: "Heavy",
|
||
none: "No Pee",
|
||
};
|
||
|
||
Object.entries(counts).forEach(([type, count]) => {
|
||
if (count > 0) {
|
||
labels.push(labelMap[type]);
|
||
data.push(count);
|
||
}
|
||
});
|
||
|
||
return { labels, data };
|
||
}
|
||
|
||
function prepareBarChartData(diaperChanges) {
|
||
const now = new Date();
|
||
const labels = [];
|
||
const poopData = [];
|
||
const peeData = [];
|
||
|
||
// Filter by selected child if applicable
|
||
let filteredChanges = diaperChanges;
|
||
if (selectedChildId !== null) {
|
||
filteredChanges = diaperChanges.filter(
|
||
(dc) => dc.child_id === selectedChildId,
|
||
);
|
||
}
|
||
|
||
// 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}`);
|
||
|
||
// 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 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 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);
|
||
|
||
// 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 };
|
||
}
|
||
|
||
function renderBarChart(chartData) {
|
||
const ctx = document.getElementById("diaperChart").getContext("2d");
|
||
|
||
// Destroy existing chart instance if it exists
|
||
if (barChartInstance) {
|
||
barChartInstance.destroy();
|
||
}
|
||
|
||
barChartInstance = new Chart(ctx, {
|
||
type: "bar",
|
||
data: {
|
||
labels: chartData.labels,
|
||
datasets: [
|
||
{
|
||
label: "💩 Poop",
|
||
data: chartData.poopData,
|
||
backgroundColor: "rgba(139, 69, 19, 0.7)",
|
||
borderColor: "rgba(139, 69, 19, 1)",
|
||
borderWidth: 2,
|
||
borderRadius: 6,
|
||
},
|
||
{
|
||
label: "💧 Pee",
|
||
data: chartData.peeData,
|
||
backgroundColor: "rgba(255, 215, 0, 0.7)",
|
||
borderColor: "rgba(255, 215, 0, 1)",
|
||
borderWidth: 2,
|
||
borderRadius: 6,
|
||
},
|
||
],
|
||
},
|
||
options: {
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
plugins: {
|
||
legend: {
|
||
display: true,
|
||
position: "top",
|
||
labels: {
|
||
padding: 15,
|
||
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.y;
|
||
return `${context.dataset.label}: ${count}`;
|
||
},
|
||
},
|
||
},
|
||
},
|
||
scales: {
|
||
y: {
|
||
beginAtZero: true,
|
||
title: {
|
||
display: true,
|
||
text: "Number of Changes",
|
||
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,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
});
|
||
}
|
||
|
||
function renderPoopPieChart(chartData) {
|
||
const ctx = document.getElementById("poopPieChart").getContext("2d");
|
||
|
||
// Destroy existing chart instance if it exists
|
||
if (poopPieChartInstance) {
|
||
poopPieChartInstance.destroy();
|
||
}
|
||
|
||
const colors = {
|
||
Light: {
|
||
bg: "rgba(139, 69, 19, 0.5)",
|
||
border: "rgba(139, 69, 19, 1)",
|
||
},
|
||
Medium: {
|
||
bg: "rgba(139, 69, 19, 0.7)",
|
||
border: "rgba(139, 69, 19, 1)",
|
||
},
|
||
Heavy: {
|
||
bg: "rgba(139, 69, 19, 0.9)",
|
||
border: "rgba(139, 69, 19, 1)",
|
||
},
|
||
"No Poop": {
|
||
bg: "rgba(200, 200, 200, 0.5)",
|
||
border: "rgba(200, 200, 200, 1)",
|
||
},
|
||
};
|
||
|
||
const backgroundColors = chartData.labels.map(
|
||
(label) => colors[label]?.bg || "rgba(200, 200, 200, 0.5)",
|
||
);
|
||
const borderColors = chartData.labels.map(
|
||
(label) => colors[label]?.border || "rgba(200, 200, 200, 1)",
|
||
);
|
||
|
||
poopPieChartInstance = 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 renderPeePieChart(chartData) {
|
||
const ctx = document.getElementById("peePieChart").getContext("2d");
|
||
|
||
// Destroy existing chart instance if it exists
|
||
if (peePieChartInstance) {
|
||
peePieChartInstance.destroy();
|
||
}
|
||
|
||
const colors = {
|
||
Light: {
|
||
bg: "rgba(255, 215, 0, 0.5)",
|
||
border: "rgba(255, 215, 0, 1)",
|
||
},
|
||
Medium: {
|
||
bg: "rgba(255, 215, 0, 0.7)",
|
||
border: "rgba(255, 215, 0, 1)",
|
||
},
|
||
Heavy: {
|
||
bg: "rgba(255, 215, 0, 0.9)",
|
||
border: "rgba(255, 215, 0, 1)",
|
||
},
|
||
"No Pee": {
|
||
bg: "rgba(200, 200, 200, 0.5)",
|
||
border: "rgba(200, 200, 200, 1)",
|
||
},
|
||
};
|
||
|
||
const backgroundColors = chartData.labels.map(
|
||
(label) => colors[label]?.bg || "rgba(200, 200, 200, 0.5)",
|
||
);
|
||
const borderColors = chartData.labels.map(
|
||
(label) => colors[label]?.border || "rgba(200, 200, 200, 1)",
|
||
);
|
||
|
||
peePieChartInstance = 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 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
|
||
loadDiaperChanges();
|
||
</script>
|
||
</body>
|
||
</html>
|