Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c007aa6f3 | ||
|
|
63fc4b618c | ||
|
|
93eb1dc458 | ||
|
|
5626408e2d | ||
|
|
fd1472c5bd | ||
|
|
5b17508ade | ||
|
|
95c2bf6744 | ||
|
|
2c4336a5e4 | ||
|
|
32946e1165 | ||
|
|
fdcfb3c8a5 | ||
|
|
1bac464268 | ||
|
|
320696c57c | ||
|
|
e26c53456f | ||
|
|
1eeb8ad148 |
@@ -281,6 +281,9 @@
|
||||
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");
|
||||
@@ -782,7 +785,12 @@
|
||||
function renderBarChart(chartData) {
|
||||
const ctx = document.getElementById("diaperChart").getContext("2d");
|
||||
|
||||
new Chart(ctx, {
|
||||
// Destroy existing chart instance if it exists
|
||||
if (barChartInstance) {
|
||||
barChartInstance.destroy();
|
||||
}
|
||||
|
||||
barChartInstance = new Chart(ctx, {
|
||||
type: "bar",
|
||||
data: {
|
||||
labels: chartData.labels,
|
||||
@@ -875,6 +883,11 @@
|
||||
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)",
|
||||
@@ -901,7 +914,7 @@
|
||||
(label) => colors[label]?.border || "rgba(200, 200, 200, 1)",
|
||||
);
|
||||
|
||||
new Chart(ctx, {
|
||||
poopPieChartInstance = new Chart(ctx, {
|
||||
type: "pie",
|
||||
data: {
|
||||
labels: chartData.labels,
|
||||
@@ -957,6 +970,11 @@
|
||||
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)",
|
||||
@@ -983,7 +1001,7 @@
|
||||
(label) => colors[label]?.border || "rgba(200, 200, 200, 1)",
|
||||
);
|
||||
|
||||
new Chart(ctx, {
|
||||
peePieChartInstance = new Chart(ctx, {
|
||||
type: "pie",
|
||||
data: {
|
||||
labels: chartData.labels,
|
||||
|
||||
@@ -319,6 +319,8 @@
|
||||
parseInt(localStorage.getItem("lastFeedingTimeRange")) || 7;
|
||||
let selectedChildId = null; // null means show first/only child
|
||||
let barChartInstance = null;
|
||||
let pieChartInstance = null;
|
||||
let durationChartInstance = null;
|
||||
|
||||
// Restore last selected child from localStorage
|
||||
const lastChildId = localStorage.getItem("lastFeedingChildFilter");
|
||||
@@ -979,6 +981,11 @@
|
||||
function renderPieChart(chartData) {
|
||||
const ctx = document.getElementById("pieChart").getContext("2d");
|
||||
|
||||
// Destroy existing chart instance if it exists
|
||||
if (pieChartInstance) {
|
||||
pieChartInstance.destroy();
|
||||
}
|
||||
|
||||
// Color mapping for each type
|
||||
const colorMap = {
|
||||
"Left Breast": {
|
||||
@@ -1003,7 +1010,7 @@
|
||||
(label) => colorMap[label]?.border || "rgba(200, 200, 200, 1)",
|
||||
);
|
||||
|
||||
new Chart(ctx, {
|
||||
pieChartInstance = new Chart(ctx, {
|
||||
type: "pie",
|
||||
data: {
|
||||
labels: chartData.labels,
|
||||
@@ -1059,7 +1066,12 @@
|
||||
function renderDurationChart(chartData) {
|
||||
const ctx = document.getElementById("durationChart").getContext("2d");
|
||||
|
||||
new Chart(ctx, {
|
||||
// Destroy existing chart instance if it exists
|
||||
if (durationChartInstance) {
|
||||
durationChartInstance.destroy();
|
||||
}
|
||||
|
||||
durationChartInstance = new Chart(ctx, {
|
||||
type: "bar",
|
||||
data: {
|
||||
labels: chartData.labels,
|
||||
|
||||
@@ -167,6 +167,52 @@
|
||||
margin-top: 0.5rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
.recent-activity {
|
||||
margin-top: 2rem;
|
||||
padding: 1.5rem;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.recent-activity h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
color: #333;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
.activity-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
.activity-card {
|
||||
background: white;
|
||||
padding: 1rem;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.activity-card .icon {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.activity-card .type {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
.activity-card .time {
|
||||
font-size: 0.85rem;
|
||||
color: #666;
|
||||
}
|
||||
.activity-card .details {
|
||||
font-size: 0.85rem;
|
||||
color: #888;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.activity-card.empty {
|
||||
opacity: 0.5;
|
||||
font-style: italic;
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -321,6 +367,7 @@
|
||||
let activeSleep = null;
|
||||
let lastSleep = null;
|
||||
let sleepUpdateTimerInterval = null;
|
||||
let lastDiaperChange = null;
|
||||
|
||||
async function loadFeedingStatus(children) {
|
||||
userChildren = children;
|
||||
@@ -337,8 +384,8 @@
|
||||
activeFeeding = await activeResponse.json();
|
||||
}
|
||||
|
||||
// Fetch all feedings to get the last one
|
||||
const feedingsResponse = await fetch("/api/feedings", {
|
||||
// Fetch the 10 most recent feedings to calculate the most recent session
|
||||
const feedingsResponse = await fetch("/api/feedings?limit=10", {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
@@ -347,8 +394,68 @@
|
||||
if (feedingsResponse.ok) {
|
||||
const feedings = await feedingsResponse.json();
|
||||
if (feedings.length > 0) {
|
||||
// Feedings are ordered by start_time desc, so first is most recent
|
||||
lastFeeding = feedings[0];
|
||||
// Calculate the most recent feeding session
|
||||
let sessionStart = feedings[0].start_time;
|
||||
let sessionEnd = feedings[0].end_time || feedings[0].start_time;
|
||||
let sessionChildId = feedings[0].child_id;
|
||||
let sessionEntries = [feedings[0]];
|
||||
|
||||
for (let i = 1; i < feedings.length; i++) {
|
||||
const entry = feedings[i];
|
||||
// Only group entries for the same child (ignore feeding type)
|
||||
if (entry.child_id !== sessionChildId) {
|
||||
break;
|
||||
}
|
||||
// If the gap between this entry's end_time and current sessionStart is more than 30 min, stop
|
||||
const prevEnd = entry.end_time || entry.start_time;
|
||||
const sessionStartDate = new Date(sessionStart);
|
||||
const prevEndDate = new Date(prevEnd);
|
||||
const diffMins = Math.abs(
|
||||
(sessionStartDate - prevEndDate) / 60000,
|
||||
);
|
||||
if (diffMins > 30) {
|
||||
break;
|
||||
}
|
||||
// Extend session to include this entry
|
||||
sessionStart = entry.start_time;
|
||||
sessionEntries.push(entry);
|
||||
}
|
||||
|
||||
// Calculate accumulated feeding time for all events in the session
|
||||
let accumulatedMinutes = 0;
|
||||
sessionEntries.forEach((entry) => {
|
||||
if (entry.end_time && entry.start_time) {
|
||||
const start = new Date(entry.start_time);
|
||||
const end = new Date(entry.end_time);
|
||||
const diffMs = end - start;
|
||||
if (diffMs > 0) {
|
||||
accumulatedMinutes += Math.floor(diffMs / 60000);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Build a string showing all feeding types used during the session
|
||||
const feedingTypesSet = new Set();
|
||||
sessionEntries.forEach((entry) => {
|
||||
if (entry.feeding_type) {
|
||||
feedingTypesSet.add(entry.feeding_type);
|
||||
}
|
||||
});
|
||||
const feedingTypesString = Array.from(feedingTypesSet)
|
||||
.map(formatFeedingType)
|
||||
.join(", ");
|
||||
|
||||
// Compose a session object
|
||||
lastFeeding = {
|
||||
child_id: sessionChildId,
|
||||
feeding_type: feedings[0].feeding_type,
|
||||
start_time: sessionStart,
|
||||
end_time: sessionEnd,
|
||||
entries: sessionEntries,
|
||||
child_name: feedings[0].child_name,
|
||||
accumulated_minutes: accumulatedMinutes,
|
||||
feeding_types_string: feedingTypesString,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -376,6 +483,20 @@
|
||||
lastSleep = sleeps[0];
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch all diaper changes to get the last one
|
||||
const diaperResponse = await fetch("/api/diaper-changes", {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (diaperResponse.ok) {
|
||||
const diapers = await diaperResponse.json();
|
||||
if (diapers.length > 0) {
|
||||
lastDiaperChange = diapers[0];
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading feeding status:", error);
|
||||
}
|
||||
@@ -449,7 +570,7 @@
|
||||
</button>
|
||||
`
|
||||
: `
|
||||
<button class="feeding-button" onclick="showStartFeedingModal()">
|
||||
<button class="feeding-button" onclick="window.location.href='/log-feeding.html'">
|
||||
🍼 Started Feeding
|
||||
</button>
|
||||
`;
|
||||
@@ -478,9 +599,100 @@
|
||||
</button>
|
||||
${feedingButtonHtml}
|
||||
${sleepButtonHtml}
|
||||
${buildRecentActivitySection()}
|
||||
`;
|
||||
}
|
||||
|
||||
function buildRecentActivitySection() {
|
||||
// Build diaper card
|
||||
const diaperCard = lastDiaperChange
|
||||
? `
|
||||
<a href="/diapers.html" class="activity-card" style="text-decoration:none; color:inherit;">
|
||||
<div class="icon">🧷</div>
|
||||
<div class="type">Last Diaper</div>
|
||||
<div class="time">${formatTime(lastDiaperChange.change_time)}</div>
|
||||
<div class="details">
|
||||
${lastDiaperChange.poop_amount ? `💩 ${lastDiaperChange.poop_amount}` : ""}
|
||||
${lastDiaperChange.poop_amount && lastDiaperChange.pee_amount ? " • " : ""}
|
||||
${lastDiaperChange.pee_amount ? `💧 ${lastDiaperChange.pee_amount}` : ""}
|
||||
</div>
|
||||
</a>
|
||||
`
|
||||
: `
|
||||
<a href="/diapers.html" class="activity-card empty" style="text-decoration:none; color:inherit;">
|
||||
<div class="icon">🧷</div>
|
||||
<div class="type">No diaper changes yet</div>
|
||||
</a>
|
||||
`;
|
||||
|
||||
// Build feeding card
|
||||
const feedingCard = lastFeeding
|
||||
? `
|
||||
<a href="/feedings.html" class="activity-card" style="text-decoration:none; color:inherit;">
|
||||
<div class="icon">🍼</div>
|
||||
<div class="type">Last Feeding Session</div>
|
||||
<div class="time">${formatTime(lastFeeding.start_time)}</div>
|
||||
<div class="details">
|
||||
${lastFeeding.feeding_types_string ? `${lastFeeding.feeding_types_string}` : ""}
|
||||
${lastFeeding.feeding_types_string && lastFeeding.accumulated_minutes ? " • " : ""}
|
||||
${lastFeeding.accumulated_minutes} minutes total
|
||||
${lastFeeding.end_time ? "" : " (ongoing)"}
|
||||
</div>
|
||||
</a>
|
||||
`
|
||||
: `
|
||||
<a href="/feedings.html" class="activity-card empty" style="text-decoration:none; color:inherit;">
|
||||
<div class="icon">🍼</div>
|
||||
<div class="type">No feedings yet</div>
|
||||
</a>
|
||||
`;
|
||||
|
||||
// Build sleep card
|
||||
const sleepCard = lastSleep
|
||||
? `
|
||||
<a href="/sleep.html" class="activity-card" style="text-decoration:none; color:inherit;">
|
||||
<div class="icon">😴</div>
|
||||
<div class="type">Last Sleep</div>
|
||||
<div class="time">${formatTime(lastSleep.start_time)}</div>
|
||||
<div class="details">
|
||||
Duration: ${lastSleep.end_time ? calculateDuration(lastSleep.start_time, lastSleep.end_time) : "ongoing"}
|
||||
</div>
|
||||
</a>
|
||||
`
|
||||
: `
|
||||
<a href="/sleep.html" class="activity-card empty" style="text-decoration:none; color:inherit;">
|
||||
<div class="icon">😴</div>
|
||||
<div class="type">No sleep sessions yet</div>
|
||||
</a>
|
||||
`;
|
||||
|
||||
return `
|
||||
<div class="recent-activity">
|
||||
<h3>📊 Recent Activity</h3>
|
||||
<div class="activity-grid">
|
||||
${diaperCard}
|
||||
${feedingCard}
|
||||
${sleepCard}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function calculateDuration(startTime, endTime) {
|
||||
const start = new Date(startTime);
|
||||
const end = new Date(endTime);
|
||||
const diffMs = end - start;
|
||||
const diffMins = Math.floor(diffMs / 60000);
|
||||
|
||||
if (diffMins < 60) {
|
||||
return `${diffMins} min`;
|
||||
}
|
||||
|
||||
const hours = Math.floor(diffMins / 60);
|
||||
const mins = diffMins % 60;
|
||||
return `${hours}h ${mins}m`;
|
||||
}
|
||||
|
||||
function formatFeedingType(type) {
|
||||
const types = {
|
||||
left_breast: "Left Breast",
|
||||
@@ -652,42 +864,7 @@
|
||||
}
|
||||
|
||||
function showStartSleepModal() {
|
||||
const modal = document.getElementById("startSleepModal");
|
||||
const childSelect = document.getElementById("modalSleepChildId");
|
||||
|
||||
// Clear and populate child select or show single child
|
||||
if (userChildren.length === 1) {
|
||||
// Single child: replace dropdown with text display
|
||||
const child = userChildren[0];
|
||||
const formGroup = childSelect.parentElement;
|
||||
formGroup.innerHTML = `
|
||||
<label for="modalSleepChildName">Child</label>
|
||||
<input
|
||||
type="text"
|
||||
id="modalSleepChildName"
|
||||
value="${child.name}"
|
||||
readonly
|
||||
style="background-color: #f5f5f5; cursor: default;"
|
||||
/>
|
||||
<input type="hidden" id="modalSleepChildId" value="${child.id}" />
|
||||
`;
|
||||
} else {
|
||||
// Multiple children: show dropdown
|
||||
childSelect.innerHTML = '<option value="">Select a child</option>';
|
||||
userChildren.forEach((child) => {
|
||||
const option = document.createElement("option");
|
||||
option.value = child.id;
|
||||
option.textContent = child.name;
|
||||
childSelect.appendChild(option);
|
||||
});
|
||||
|
||||
// Set default based on last sleep
|
||||
if (lastSleep) {
|
||||
childSelect.value = lastSleep.child_id;
|
||||
}
|
||||
}
|
||||
|
||||
modal.classList.add("show");
|
||||
window.location.href = "/log-sleep.html";
|
||||
}
|
||||
|
||||
function closeStartSleepModal() {
|
||||
|
||||
@@ -336,6 +336,29 @@
|
||||
lastDiaperChange.child_id;
|
||||
}
|
||||
}
|
||||
|
||||
// Prepopulate poop and pee fields from most recent entry
|
||||
try {
|
||||
const response = await fetch("/api/diaper-changes", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (response.ok) {
|
||||
const changes = await response.json();
|
||||
if (changes.length > 0) {
|
||||
const last = changes[0];
|
||||
document.getElementById("poopAmount").value =
|
||||
last.poop_amount || "";
|
||||
document.getElementById("poopColor").value =
|
||||
last.poop_color || "";
|
||||
document.getElementById("peeAmount").value =
|
||||
last.pee_amount || "";
|
||||
document.getElementById("peeColor").value =
|
||||
last.pee_color || "";
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore error, just use default
|
||||
}
|
||||
}
|
||||
} else {
|
||||
showMessage("Failed to load children", "error");
|
||||
|
||||
@@ -303,6 +303,22 @@
|
||||
// If editing, load the feeding data
|
||||
if (isEditMode) {
|
||||
loadFeedingData();
|
||||
} else {
|
||||
// Set default feeding type to most recent log entry
|
||||
try {
|
||||
const response = await fetch("/api/feedings", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (response.ok) {
|
||||
const feedings = await response.json();
|
||||
if (feedings.length > 0) {
|
||||
document.getElementById("feedingType").value =
|
||||
feedings[0].feeding_type;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore error, just use default
|
||||
}
|
||||
}
|
||||
} else {
|
||||
showMessage("Failed to load children", "error");
|
||||
|
||||
@@ -1104,6 +1104,9 @@
|
||||
</div>
|
||||
<div class="sleep-duration">${durationText}</div>
|
||||
</div>
|
||||
<div class="sleep-actions">
|
||||
<button class="edit-button" onclick="window.location.href='/log-sleep.html?id=${sleep.id}'">Edit</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user