Merge pull request 'Improved single child UX' (#29) from improve-single-child-UX into main
Build and Push Docker Image / build-and-push (push) Successful in 25s
Python Code Quality / python-code-quality (push) Successful in 9s
Python Test / python-test (push) Successful in 18s

Reviewed-on: #29
This commit was merged in pull request #29.
This commit is contained in:
2025-11-12 16:42:30 +01:00
7 changed files with 294 additions and 95 deletions
+22 -8
View File
@@ -280,7 +280,7 @@
let allChildren = []; let allChildren = [];
let currentDaysRange = let currentDaysRange =
parseInt(localStorage.getItem("lastDiaperTimeRange")) || 7; parseInt(localStorage.getItem("lastDiaperTimeRange")) || 7;
let selectedChildId = null; // null means "All Children" let selectedChildId = null; // null means show first/only child
// Restore last selected child from localStorage // Restore last selected child from localStorage
const lastChildId = localStorage.getItem("lastDiaperChildFilter"); const lastChildId = localStorage.getItem("lastDiaperChildFilter");
@@ -321,6 +321,11 @@
if (response.ok) { if (response.ok) {
allChildren = await response.json(); allChildren = await response.json();
// Auto-select first child if no selection exists
if (selectedChildId === null && allChildren.length > 0) {
selectedChildId = allChildren[0].id;
}
} }
} catch (error) { } catch (error) {
console.error("Error loading children:", error); console.error("Error loading children:", error);
@@ -334,8 +339,7 @@
} }
function onChildChange(event) { function onChildChange(event) {
selectedChildId = selectedChildId = parseInt(event.target.value);
event.target.value === "all" ? null : parseInt(event.target.value);
localStorage.setItem("lastDiaperChildFilter", event.target.value); localStorage.setItem("lastDiaperChildFilter", event.target.value);
displayDiaperChanges(allDiaperChanges); displayDiaperChanges(allDiaperChanges);
} }
@@ -367,11 +371,21 @@
content.innerHTML = ` content.innerHTML = `
<div class="controls"> <div class="controls">
<label for="childFilter">Child:</label> ${allChildren.length === 1 ? `
<select id="childFilter" onchange="onChildChange(event)"> <label for="childDisplay">Child:</label>
<option value="all" ${selectedChildId === null ? "selected" : ""}>All Children</option> <input
${childOptions} type="text"
</select> 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> <label for="daysRange">Time Range:</label>
<select id="daysRange" onchange="onDaysRangeChange(event)"> <select id="daysRange" onchange="onDaysRangeChange(event)">
+22 -8
View File
@@ -317,7 +317,7 @@
let allChildren = []; let allChildren = [];
let currentDaysRange = let currentDaysRange =
parseInt(localStorage.getItem("lastFeedingTimeRange")) || 7; parseInt(localStorage.getItem("lastFeedingTimeRange")) || 7;
let selectedChildId = null; // null means "All Children" let selectedChildId = null; // null means show first/only child
let barChartInstance = null; let barChartInstance = null;
// Restore last selected child from localStorage // Restore last selected child from localStorage
@@ -359,6 +359,11 @@
if (response.ok) { if (response.ok) {
allChildren = await response.json(); allChildren = await response.json();
// Auto-select first child if no selection exists
if (selectedChildId === null && allChildren.length > 0) {
selectedChildId = allChildren[0].id;
}
} }
} catch (error) { } catch (error) {
console.error("Error loading children:", error); console.error("Error loading children:", error);
@@ -372,8 +377,7 @@
} }
function onChildChange(event) { function onChildChange(event) {
selectedChildId = selectedChildId = parseInt(event.target.value);
event.target.value === "all" ? null : parseInt(event.target.value);
localStorage.setItem("lastFeedingChildFilter", event.target.value); localStorage.setItem("lastFeedingChildFilter", event.target.value);
displayFeedings(allFeedings); displayFeedings(allFeedings);
} }
@@ -405,11 +409,21 @@
content.innerHTML = ` content.innerHTML = `
<div class="controls"> <div class="controls">
<label for="childFilter">Child:</label> ${allChildren.length === 1 ? `
<select id="childFilter" onchange="onChildChange(event)"> <label for="childDisplay">Child:</label>
<option value="all" ${selectedChildId === null ? "selected" : ""}>All Children</option> <input
${childOptions} type="text"
</select> 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> <label for="daysRange">Time Range:</label>
<select id="daysRange" onchange="onDaysRangeChange(event)"> <select id="daysRange" onchange="onDaysRangeChange(event)">
+61 -21
View File
@@ -510,18 +510,40 @@
const childSelect = document.getElementById("modalChildId"); const childSelect = document.getElementById("modalChildId");
const typeSelect = document.getElementById("modalFeedingType"); const typeSelect = document.getElementById("modalFeedingType");
// Clear and populate child select // Clear and populate child select or show single child
childSelect.innerHTML = '<option value="">Select a child</option>'; if (userChildren.length === 1) {
userChildren.forEach((child) => { // Single child: replace dropdown with text display
const option = document.createElement("option"); const child = userChildren[0];
option.value = child.id; const formGroup = childSelect.parentElement;
option.textContent = child.name; formGroup.innerHTML = `
childSelect.appendChild(option); <label for="modalChildName">Child</label>
}); <input
type="text"
id="modalChildName"
value="${child.name}"
readonly
style="background-color: #f5f5f5; cursor: default;"
/>
<input type="hidden" id="modalChildId" 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 defaults based on last feeding // Set defaults based on last feeding
if (lastFeeding) {
childSelect.value = lastFeeding.child_id;
}
}
// Set default feeding type based on last feeding
if (lastFeeding) { if (lastFeeding) {
childSelect.value = lastFeeding.child_id;
typeSelect.value = lastFeeding.feeding_type; typeSelect.value = lastFeeding.feeding_type;
} }
@@ -633,18 +655,36 @@
const modal = document.getElementById("startSleepModal"); const modal = document.getElementById("startSleepModal");
const childSelect = document.getElementById("modalSleepChildId"); const childSelect = document.getElementById("modalSleepChildId");
// Clear and populate child select // Clear and populate child select or show single child
childSelect.innerHTML = '<option value="">Select a child</option>'; if (userChildren.length === 1) {
userChildren.forEach((child) => { // Single child: replace dropdown with text display
const option = document.createElement("option"); const child = userChildren[0];
option.value = child.id; const formGroup = childSelect.parentElement;
option.textContent = child.name; formGroup.innerHTML = `
childSelect.appendChild(option); <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 // Set default based on last sleep
if (lastSleep) { if (lastSleep) {
childSelect.value = lastSleep.child_id; childSelect.value = lastSleep.child_id;
}
} }
modal.classList.add("show"); modal.classList.add("show");
+47 -15
View File
@@ -235,7 +235,7 @@
<button <button
type="button" type="button"
class="button secondary" class="button secondary"
onclick="window.location.href='/diapers.html'" onclick="window.location.href=getReturnUrl()"
> >
Cancel Cancel
</button> </button>
@@ -249,6 +249,16 @@
window.location.href = "/login.html"; window.location.href = "/login.html";
} }
// Determine return URL based on referrer
function getReturnUrl() {
const referrer = document.referrer;
if (referrer.includes('/diapers.html')) {
return '/diapers.html';
}
// Default to home page
return '/';
}
// Check if we're in edit mode // Check if we're in edit mode
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
const diaperChangeId = urlParams.get("id"); const diaperChangeId = urlParams.get("id");
@@ -277,12 +287,32 @@
children = await response.json(); children = await response.json();
const select = document.getElementById("childSelect"); const select = document.getElementById("childSelect");
children.forEach((child) => { // Populate select dropdown or show single child
const option = document.createElement("option"); if (children.length === 1) {
option.value = child.id; // Single child: replace dropdown with text display
option.textContent = child.name; const child = children[0];
select.appendChild(option); const formGroup = select.parentElement;
}); formGroup.innerHTML = `
<label for="childName">Child *</label>
<input
type="text"
id="childName"
value="${child.name}"
readonly
style="background-color: #f5f5f5; cursor: default;"
/>
<input type="hidden" id="childSelect" name="childSelect" value="${child.id}" />
`;
} else {
// Multiple children: show dropdown (clear the placeholder first)
select.innerHTML = '';
children.forEach((child) => {
const option = document.createElement("option");
option.value = child.id;
option.textContent = child.name;
select.appendChild(option);
});
}
// If editing, load the diaper change data // If editing, load the diaper change data
if (isEditMode) { if (isEditMode) {
@@ -295,14 +325,16 @@
.toISOString() .toISOString()
.slice(0, 16); .slice(0, 16);
// Load last used child from localStorage // Load last used child from localStorage (only for multiple children)
const lastDiaperChange = JSON.parse( if (children.length > 1) {
localStorage.getItem("lastDiaperChange") || "{}", const lastDiaperChange = JSON.parse(
); localStorage.getItem("lastDiaperChange") || "{}",
);
if (lastDiaperChange.child_id) { if (lastDiaperChange.child_id) {
document.getElementById("childSelect").value = document.getElementById("childSelect").value =
lastDiaperChange.child_id; lastDiaperChange.child_id;
}
} }
} }
} else { } else {
@@ -431,7 +463,7 @@
"success", "success",
); );
setTimeout(() => { setTimeout(() => {
window.location.href = "/diapers.html"; window.location.href = getReturnUrl();
}, 1000); }, 1000);
} else { } else {
const errorData = await response.json(); const errorData = await response.json();
+39 -10
View File
@@ -206,6 +206,16 @@
window.location.href = "/login.html"; window.location.href = "/login.html";
} }
// Determine return URL based on referrer
function getReturnUrl() {
const referrer = document.referrer;
if (referrer.includes('/feedings.html')) {
return '/feedings.html';
}
// Default to home page
return '/';
}
const form = document.getElementById("logFeedingForm"); const form = document.getElementById("logFeedingForm");
const messageDiv = document.getElementById("message"); const messageDiv = document.getElementById("message");
const submitBtn = document.getElementById("submitBtn"); const submitBtn = document.getElementById("submitBtn");
@@ -259,13 +269,32 @@
return; return;
} }
// Populate select dropdown // Populate select dropdown or show single child
children.forEach((child) => { if (children.length === 1) {
const option = document.createElement("option"); // Single child: replace dropdown with text display
option.value = child.id; const child = children[0];
option.textContent = child.name; const formGroup = childSelect.parentElement;
childSelect.appendChild(option); formGroup.innerHTML = `
}); <label for="childName">Child</label>
<input
type="text"
id="childName"
value="${child.name}"
readonly
style="background-color: #f5f5f5; cursor: default;"
/>
<input type="hidden" id="childId" name="childId" value="${child.id}" />
`;
} else {
// Multiple children: show dropdown (clear placeholder first)
childSelect.innerHTML = '';
children.forEach((child) => {
const option = document.createElement("option");
option.value = child.id;
option.textContent = child.name;
childSelect.appendChild(option);
});
}
// Show form, hide loading // Show form, hide loading
loadingDiv.style.display = "none"; loadingDiv.style.display = "none";
@@ -369,9 +398,9 @@
: "Feeding logged successfully!", : "Feeding logged successfully!",
"success", "success",
); );
// Redirect to feedings page after 1 second // Redirect to appropriate page after 1 second
setTimeout(() => { setTimeout(() => {
window.location.href = "/feedings.html"; window.location.href = getReturnUrl();
}, 1000); }, 1000);
} else { } else {
showMessage( showMessage(
@@ -394,7 +423,7 @@
} }
function goBack() { function goBack() {
window.location.href = "/feedings.html"; window.location.href = getReturnUrl();
} }
</script> </script>
</body> </body>
+39 -10
View File
@@ -196,6 +196,16 @@
window.location.href = "/login.html"; window.location.href = "/login.html";
} }
// Determine return URL based on referrer
function getReturnUrl() {
const referrer = document.referrer;
if (referrer.includes('/sleep.html')) {
return '/sleep.html';
}
// Default to home page
return '/';
}
const form = document.getElementById("logSleepForm"); const form = document.getElementById("logSleepForm");
const messageDiv = document.getElementById("message"); const messageDiv = document.getElementById("message");
const submitBtn = document.getElementById("submitBtn"); const submitBtn = document.getElementById("submitBtn");
@@ -249,13 +259,32 @@
return; return;
} }
// Populate select dropdown // Populate select dropdown or show single child
children.forEach((child) => { if (children.length === 1) {
const option = document.createElement("option"); // Single child: replace dropdown with text display
option.value = child.id; const child = children[0];
option.textContent = child.name; const formGroup = childSelect.parentElement;
childSelect.appendChild(option); formGroup.innerHTML = `
}); <label for="childName">Child</label>
<input
type="text"
id="childName"
value="${child.name}"
readonly
style="background-color: #f5f5f5; cursor: default;"
/>
<input type="hidden" id="childId" name="childId" value="${child.id}" />
`;
} else {
// Multiple children: show dropdown (clear placeholder first)
childSelect.innerHTML = '';
children.forEach((child) => {
const option = document.createElement("option");
option.value = child.id;
option.textContent = child.name;
childSelect.appendChild(option);
});
}
// Show form, hide loading // Show form, hide loading
loadingDiv.style.display = "none"; loadingDiv.style.display = "none";
@@ -353,9 +382,9 @@
: "Sleep logged successfully!", : "Sleep logged successfully!",
"success", "success",
); );
// Redirect to sleep page after 1 second // Redirect to appropriate page after 1 second
setTimeout(() => { setTimeout(() => {
window.location.href = "/sleep.html"; window.location.href = getReturnUrl();
}, 1000); }, 1000);
} else { } else {
showMessage( showMessage(
@@ -378,7 +407,7 @@
} }
function goBack() { function goBack() {
window.location.href = "/sleep.html"; window.location.href = getReturnUrl();
} }
</script> </script>
</body> </body>
+64 -23
View File
@@ -328,19 +328,22 @@
let allChildren = []; let allChildren = [];
let selectedChildId = ""; let selectedChildId = "";
// Filter change handlers // Filter change handlers (will be attached after children are loaded)
document function attachChildFilterListener() {
.getElementById("childFilter") const childFilter = document.getElementById("childFilter");
.addEventListener("change", function () { if (childFilter) {
selectedChildId = this.value; childFilter.addEventListener("change", function () {
// Save selection to localStorage selectedChildId = this.value;
if (selectedChildId) { // Save selection to localStorage
localStorage.setItem("lastSleepChildFilter", selectedChildId); if (selectedChildId) {
} else { localStorage.setItem("lastSleepChildFilter", selectedChildId);
localStorage.removeItem("lastSleepChildFilter"); } else {
} localStorage.removeItem("lastSleepChildFilter");
renderCharts(); }
}); renderCharts();
});
}
}
document document
.getElementById("timeRangeFilter") .getElementById("timeRangeFilter")
@@ -365,19 +368,52 @@
allChildren = await childrenResponse.json(); allChildren = await childrenResponse.json();
// Populate child filter // Auto-select first child if available
const childFilter = document.getElementById("childFilter"); if (allChildren.length > 0 && !selectedChildId) {
childFilter.innerHTML = '<option value="">All Children</option>'; selectedChildId = allChildren[0].id;
allChildren.forEach((child) => { }
const option = document.createElement("option");
option.value = child.id; // Populate child filter or show single child
option.textContent = child.name; const childFilterContainer = document.querySelector('.controls');
childFilter.appendChild(option); if (allChildren.length === 1) {
}); // Single child: show as read-only text
const child = allChildren[0];
childFilterContainer.innerHTML = `
<label for="childDisplay">Child:</label>
<input
type="text"
id="childDisplay"
value="${child.name}"
readonly
style="background-color: #f5f5f5; cursor: default; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;"
/>
<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>
`;
} else {
// Multiple children: show dropdown
const childFilter = document.getElementById("childFilter");
childFilter.innerHTML = '';
allChildren.forEach((child) => {
const option = document.createElement("option");
option.value = child.id;
option.textContent = child.name;
childFilter.appendChild(option);
});
}
// Restore last selected child from localStorage // Restore last selected child from localStorage
const lastChildId = localStorage.getItem("lastSleepChildFilter"); const lastChildId = localStorage.getItem("lastSleepChildFilter");
if (lastChildId) { if (lastChildId && allChildren.length > 1) {
const childFilter = document.getElementById("childFilter");
childFilter.value = lastChildId; childFilter.value = lastChildId;
selectedChildId = lastChildId; selectedChildId = lastChildId;
} }
@@ -402,6 +438,11 @@
allSleeps = await sleepResponse.json(); allSleeps = await sleepResponse.json();
// Attach event listener for child filter if multiple children
if (allChildren.length > 1) {
attachChildFilterListener();
}
renderCharts(); renderCharts();
} catch (error) { } catch (error) {
console.error("Error loading data:", error); console.error("Error loading data:", error);