752 lines
22 KiB
HTML
752 lines
22 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>Baby Monitor</title>
|
|
<link rel="stylesheet" href="/menu.css" />
|
|
<style>
|
|
body {
|
|
font-family: Arial, sans-serif;
|
|
max-width: 800px;
|
|
margin: 2rem auto;
|
|
padding: 0 1rem;
|
|
background-color: #f0f0f0;
|
|
}
|
|
.container {
|
|
background: white;
|
|
padding: 2rem;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
|
}
|
|
h1 {
|
|
color: #333;
|
|
margin-bottom: 1rem;
|
|
}
|
|
.user-info {
|
|
background: #e3f2fd;
|
|
padding: 1rem;
|
|
border-radius: 4px;
|
|
margin-bottom: 1rem;
|
|
}
|
|
button {
|
|
padding: 0.75rem 1.5rem;
|
|
background-color: #dc3545;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 1rem;
|
|
margin-right: 0.5rem;
|
|
}
|
|
button:hover {
|
|
background-color: #c82333;
|
|
}
|
|
.admin-button {
|
|
background-color: #667eea;
|
|
}
|
|
.admin-button:hover {
|
|
background-color: #5568d3;
|
|
}
|
|
.hidden {
|
|
display: none;
|
|
}
|
|
.loading {
|
|
text-align: center;
|
|
padding: 2rem;
|
|
}
|
|
.feeding-button {
|
|
width: 100%;
|
|
padding: 2rem;
|
|
font-size: 1.5rem;
|
|
margin-bottom: 1.5rem;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
border-radius: 8px;
|
|
transition:
|
|
transform 0.2s,
|
|
box-shadow 0.2s;
|
|
}
|
|
.feeding-button:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
|
|
background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
|
|
}
|
|
.feeding-button.active {
|
|
background: linear-gradient(135deg, #f44336 0%, #e91e63 100%);
|
|
}
|
|
.feeding-button.active:hover {
|
|
background: linear-gradient(135deg, #e91e63 0%, #f44336 100%);
|
|
}
|
|
.feeding-info {
|
|
font-size: 0.9rem;
|
|
margin-top: 0.5rem;
|
|
opacity: 0.9;
|
|
}
|
|
.modal {
|
|
display: none;
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: rgba(0, 0, 0, 0.5);
|
|
z-index: 1001;
|
|
justify-content: center;
|
|
align-items: center;
|
|
}
|
|
.modal.show {
|
|
display: flex;
|
|
}
|
|
.modal-content {
|
|
background: white;
|
|
padding: 2rem;
|
|
border-radius: 8px;
|
|
max-width: 400px;
|
|
width: 90%;
|
|
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
|
|
}
|
|
.modal h2 {
|
|
margin-top: 0;
|
|
margin-bottom: 1.5rem;
|
|
color: #333;
|
|
}
|
|
.form-group {
|
|
margin-bottom: 1rem;
|
|
}
|
|
.form-group label {
|
|
display: block;
|
|
margin-bottom: 0.5rem;
|
|
color: #333;
|
|
font-weight: 600;
|
|
}
|
|
.form-group select {
|
|
width: 100%;
|
|
padding: 0.75rem;
|
|
border: 1px solid #ddd;
|
|
border-radius: 4px;
|
|
font-size: 1rem;
|
|
}
|
|
.modal-buttons {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
margin-top: 1.5rem;
|
|
}
|
|
.modal-buttons button {
|
|
flex: 1;
|
|
margin: 0;
|
|
}
|
|
.cancel-button {
|
|
background-color: #6c757d;
|
|
}
|
|
.cancel-button:hover {
|
|
background-color: #5a6268;
|
|
}
|
|
.sleep-button {
|
|
width: 100%;
|
|
padding: 2rem;
|
|
font-size: 1.5rem;
|
|
margin-bottom: 1.5rem;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
border-radius: 8px;
|
|
transition:
|
|
transform 0.2s,
|
|
box-shadow 0.2s;
|
|
}
|
|
.sleep-button:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
|
|
background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
|
|
}
|
|
.sleep-button.active {
|
|
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
|
}
|
|
.sleep-button.active:hover {
|
|
background: linear-gradient(135deg, #00f2fe 0%, #4facfe 100%);
|
|
}
|
|
.sleep-info {
|
|
font-size: 0.9rem;
|
|
margin-top: 0.5rem;
|
|
opacity: 0.9;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div id="content" class="loading">
|
|
<p>Loading...</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Start Feeding Modal -->
|
|
<div class="modal" id="startFeedingModal">
|
|
<div class="modal-content">
|
|
<h2>🍼 Start Feeding</h2>
|
|
<form id="startFeedingForm">
|
|
<div class="form-group">
|
|
<label for="modalChildId">Child</label>
|
|
<select id="modalChildId" required>
|
|
<option value="">Select a child</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="modalFeedingType">Feeding Type</label>
|
|
<select id="modalFeedingType" required>
|
|
<option value="">Select type</option>
|
|
<option value="left_breast">Left Breast</option>
|
|
<option value="right_breast">Right Breast</option>
|
|
<option value="bottle">Bottle</option>
|
|
</select>
|
|
</div>
|
|
<div class="modal-buttons">
|
|
<button
|
|
type="button"
|
|
class="cancel-button"
|
|
onclick="closeStartFeedingModal()"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button type="submit">Start</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Start Sleep Modal -->
|
|
<div class="modal" id="startSleepModal">
|
|
<div class="modal-content">
|
|
<h2>😴 Start Sleep</h2>
|
|
<form id="startSleepForm">
|
|
<div class="form-group">
|
|
<label for="modalSleepChildId">Child</label>
|
|
<select id="modalSleepChildId" required>
|
|
<option value="">Select a child</option>
|
|
</select>
|
|
</div>
|
|
<div class="modal-buttons">
|
|
<button
|
|
type="button"
|
|
class="cancel-button"
|
|
onclick="closeStartSleepModal()"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button type="submit">Start</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="/menu.js"></script>
|
|
<script>
|
|
const token = localStorage.getItem("access_token");
|
|
const username = localStorage.getItem("username");
|
|
|
|
// Check if user is logged in
|
|
if (!token) {
|
|
window.location.href = "/login.html";
|
|
} else {
|
|
// Fetch current user info including admin status
|
|
fetch("/api/me", {
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
})
|
|
.then((response) => {
|
|
if (response.ok) {
|
|
return response.json();
|
|
} else {
|
|
// Token invalid, redirect to login
|
|
localStorage.removeItem("access_token");
|
|
localStorage.removeItem("username");
|
|
window.location.href = "/login.html";
|
|
throw new Error("Authentication failed");
|
|
}
|
|
})
|
|
.then((user) => {
|
|
// Initialize burger menu
|
|
initBurgerMenu({
|
|
includeHome: false,
|
|
});
|
|
|
|
// Check if user has any children
|
|
return fetch("/api/children", {
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
})
|
|
.then((response) => response.json())
|
|
.then((children) => {
|
|
// If no children, redirect to add-child page
|
|
if (children.length === 0) {
|
|
window.location.href = "/add-child.html";
|
|
return;
|
|
}
|
|
|
|
// Load feeding status and show content
|
|
loadFeedingStatus(children);
|
|
});
|
|
})
|
|
.catch((error) => {
|
|
console.error("Error:", error);
|
|
});
|
|
}
|
|
|
|
let activeFeeding = null;
|
|
let userChildren = [];
|
|
let lastFeeding = null;
|
|
let updateTimerInterval = null;
|
|
let activeSleep = null;
|
|
let lastSleep = null;
|
|
let sleepUpdateTimerInterval = null;
|
|
|
|
async function loadFeedingStatus(children) {
|
|
userChildren = children;
|
|
|
|
try {
|
|
// Fetch active feeding
|
|
const activeResponse = await fetch("/api/feedings/active", {
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
});
|
|
|
|
if (activeResponse.ok) {
|
|
activeFeeding = await activeResponse.json();
|
|
}
|
|
|
|
// Fetch all feedings to get the last one
|
|
const feedingsResponse = await fetch("/api/feedings", {
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
});
|
|
|
|
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];
|
|
}
|
|
}
|
|
|
|
// Fetch active sleep
|
|
const activeSleepResponse = await fetch("/api/sleep/active", {
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
});
|
|
|
|
if (activeSleepResponse.ok) {
|
|
activeSleep = await activeSleepResponse.json();
|
|
}
|
|
|
|
// Fetch all sleep logs to get the last one
|
|
const sleepResponse = await fetch("/api/sleep", {
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
});
|
|
|
|
if (sleepResponse.ok) {
|
|
const sleeps = await sleepResponse.json();
|
|
if (sleeps.length > 0) {
|
|
lastSleep = sleeps[0];
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error("Error loading feeding status:", error);
|
|
}
|
|
|
|
renderContent();
|
|
startUpdateTimer();
|
|
startSleepUpdateTimer();
|
|
}
|
|
|
|
function startUpdateTimer() {
|
|
// Clear any existing timer
|
|
if (updateTimerInterval) {
|
|
clearInterval(updateTimerInterval);
|
|
}
|
|
|
|
// Update every 30 seconds if there's an active feeding
|
|
if (activeFeeding) {
|
|
updateTimerInterval = setInterval(() => {
|
|
if (activeFeeding) {
|
|
updateFeedingTimeDisplay();
|
|
} else {
|
|
clearInterval(updateTimerInterval);
|
|
updateTimerInterval = null;
|
|
}
|
|
}, 30000); // Update every 30 seconds
|
|
}
|
|
}
|
|
|
|
function startSleepUpdateTimer() {
|
|
// Clear any existing timer
|
|
if (sleepUpdateTimerInterval) {
|
|
clearInterval(sleepUpdateTimerInterval);
|
|
}
|
|
|
|
// Update every 30 seconds if there's an active sleep
|
|
if (activeSleep) {
|
|
sleepUpdateTimerInterval = setInterval(() => {
|
|
if (activeSleep) {
|
|
updateSleepTimeDisplay();
|
|
} else {
|
|
clearInterval(sleepUpdateTimerInterval);
|
|
sleepUpdateTimerInterval = null;
|
|
}
|
|
}, 30000); // Update every 30 seconds
|
|
}
|
|
}
|
|
|
|
function updateFeedingTimeDisplay() {
|
|
const timeElement = document.getElementById("feedingTimeInfo");
|
|
if (timeElement && activeFeeding) {
|
|
timeElement.textContent = formatTime(activeFeeding.start_time);
|
|
}
|
|
}
|
|
|
|
function updateSleepTimeDisplay() {
|
|
const timeElement = document.getElementById("sleepTimeInfo");
|
|
if (timeElement && activeSleep) {
|
|
timeElement.textContent = formatTime(activeSleep.start_time);
|
|
}
|
|
}
|
|
|
|
function renderContent() {
|
|
const feedingButtonHtml = activeFeeding
|
|
? `
|
|
<button class="feeding-button active" onclick="stopFeeding()">
|
|
🛑 Stopped Feeding
|
|
<div class="feeding-info">
|
|
${activeFeeding.child_name || "Child"} - ${formatFeedingType(activeFeeding.feeding_type)}<br>
|
|
Started <span id="feedingTimeInfo">${formatTime(activeFeeding.start_time)}</span>
|
|
</div>
|
|
</button>
|
|
`
|
|
: `
|
|
<button class="feeding-button" onclick="showStartFeedingModal()">
|
|
🍼 Started Feeding
|
|
</button>
|
|
`;
|
|
|
|
const sleepButtonHtml = activeSleep
|
|
? `
|
|
<button class="sleep-button active" onclick="stopSleep()">
|
|
🛑 Stopped Sleeping
|
|
<div class="sleep-info">
|
|
${activeSleep.child_name || "Child"}<br>
|
|
Started <span id="sleepTimeInfo">${formatTime(activeSleep.start_time)}</span>
|
|
</div>
|
|
</button>
|
|
`
|
|
: `
|
|
<button class="sleep-button" onclick="showStartSleepModal()">
|
|
😴 Started Sleeping
|
|
</button>
|
|
`;
|
|
|
|
document.getElementById("content").innerHTML = `
|
|
<h1>Welcome to Baby Monitor!</h1>
|
|
<div class="user-info">
|
|
<p><strong>Logged in as:</strong> ${username}</p>
|
|
</div>
|
|
<button class="feeding-button" onclick="window.location.href='/log-diaper.html'" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); margin-bottom: 20px;">
|
|
🧷 Changed Diaper
|
|
</button>
|
|
${feedingButtonHtml}
|
|
${sleepButtonHtml}
|
|
`;
|
|
}
|
|
|
|
function formatFeedingType(type) {
|
|
const types = {
|
|
left_breast: "Left Breast",
|
|
right_breast: "Right Breast",
|
|
bottle: "Bottle",
|
|
};
|
|
return types[type] || type;
|
|
}
|
|
|
|
function formatTime(dateString) {
|
|
const date = new Date(dateString);
|
|
const now = new Date();
|
|
const diffMs = now - date;
|
|
const diffMins = Math.floor(diffMs / 60000);
|
|
|
|
if (diffMins < 1) return "just now";
|
|
if (diffMins < 60) return `${diffMins} min ago`;
|
|
|
|
const diffHours = Math.floor(diffMins / 60);
|
|
if (diffHours < 24) return `${diffHours}h ${diffMins % 60}m ago`;
|
|
|
|
return date.toLocaleString();
|
|
}
|
|
|
|
function showStartFeedingModal() {
|
|
const modal = document.getElementById("startFeedingModal");
|
|
const childSelect = document.getElementById("modalChildId");
|
|
const typeSelect = document.getElementById("modalFeedingType");
|
|
|
|
// Clear and populate child select
|
|
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
|
|
if (lastFeeding) {
|
|
childSelect.value = lastFeeding.child_id;
|
|
typeSelect.value = lastFeeding.feeding_type;
|
|
}
|
|
|
|
modal.classList.add("show");
|
|
}
|
|
|
|
function closeStartFeedingModal() {
|
|
const modal = document.getElementById("startFeedingModal");
|
|
modal.classList.remove("show");
|
|
document.getElementById("startFeedingForm").reset();
|
|
}
|
|
|
|
document
|
|
.getElementById("startFeedingForm")
|
|
.addEventListener("submit", async (e) => {
|
|
e.preventDefault();
|
|
|
|
const childId = parseInt(
|
|
document.getElementById("modalChildId").value,
|
|
);
|
|
const feedingType = document.getElementById("modalFeedingType").value;
|
|
|
|
try {
|
|
const now = new Date();
|
|
const localDateTime = new Date(
|
|
now.getTime() - now.getTimezoneOffset() * 60000,
|
|
)
|
|
.toISOString()
|
|
.slice(0, 19);
|
|
|
|
const response = await fetch("/api/feedings", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
body: JSON.stringify({
|
|
child_id: childId,
|
|
start_time: localDateTime,
|
|
end_time: null,
|
|
feeding_type: feedingType,
|
|
}),
|
|
});
|
|
|
|
if (response.ok) {
|
|
const feeding = await response.json();
|
|
activeFeeding = feeding;
|
|
lastFeeding = feeding; // Update last feeding
|
|
|
|
// Add child name to active feeding
|
|
const child = userChildren.find((c) => c.id === childId);
|
|
if (child) {
|
|
activeFeeding.child_name = child.name;
|
|
}
|
|
|
|
closeStartFeedingModal();
|
|
renderContent();
|
|
startUpdateTimer(); // Start the timer for live updates
|
|
} else {
|
|
const data = await response.json();
|
|
alert(data.detail || "Failed to start feeding");
|
|
}
|
|
} catch (error) {
|
|
console.error("Error starting feeding:", error);
|
|
alert("Network error. Please try again.");
|
|
}
|
|
});
|
|
|
|
async function stopFeeding() {
|
|
if (!activeFeeding) return;
|
|
|
|
try {
|
|
const now = new Date();
|
|
const localDateTime = new Date(
|
|
now.getTime() - now.getTimezoneOffset() * 60000,
|
|
)
|
|
.toISOString()
|
|
.slice(0, 19);
|
|
|
|
const response = await fetch(`/api/feedings/${activeFeeding.id}`, {
|
|
method: "PUT",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
body: JSON.stringify({
|
|
end_time: localDateTime,
|
|
}),
|
|
});
|
|
|
|
if (response.ok) {
|
|
activeFeeding = null;
|
|
if (updateTimerInterval) {
|
|
clearInterval(updateTimerInterval);
|
|
updateTimerInterval = null;
|
|
}
|
|
renderContent();
|
|
} else {
|
|
const data = await response.json();
|
|
alert(data.detail || "Failed to stop feeding");
|
|
}
|
|
} catch (error) {
|
|
console.error("Error stopping feeding:", error);
|
|
alert("Network error. Please try again.");
|
|
}
|
|
}
|
|
|
|
function showStartSleepModal() {
|
|
const modal = document.getElementById("startSleepModal");
|
|
const childSelect = document.getElementById("modalSleepChildId");
|
|
|
|
// Clear and populate child select
|
|
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");
|
|
}
|
|
|
|
function closeStartSleepModal() {
|
|
const modal = document.getElementById("startSleepModal");
|
|
modal.classList.remove("show");
|
|
document.getElementById("startSleepForm").reset();
|
|
}
|
|
|
|
document
|
|
.getElementById("startSleepForm")
|
|
.addEventListener("submit", async (e) => {
|
|
e.preventDefault();
|
|
|
|
const childId = parseInt(
|
|
document.getElementById("modalSleepChildId").value,
|
|
);
|
|
|
|
try {
|
|
const now = new Date();
|
|
const localDateTime = new Date(
|
|
now.getTime() - now.getTimezoneOffset() * 60000,
|
|
)
|
|
.toISOString()
|
|
.slice(0, 19);
|
|
|
|
const response = await fetch("/api/sleep", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
body: JSON.stringify({
|
|
child_id: childId,
|
|
start_time: localDateTime,
|
|
}),
|
|
});
|
|
|
|
if (response.ok) {
|
|
const sleep = await response.json();
|
|
activeSleep = sleep;
|
|
lastSleep = sleep;
|
|
|
|
// Add child name to active sleep
|
|
const child = userChildren.find((c) => c.id === childId);
|
|
if (child) {
|
|
activeSleep.child_name = child.name;
|
|
}
|
|
|
|
closeStartSleepModal();
|
|
renderContent();
|
|
startSleepUpdateTimer();
|
|
} else {
|
|
const data = await response.json();
|
|
alert(data.detail || "Failed to start sleep");
|
|
}
|
|
} catch (error) {
|
|
console.error("Error starting sleep:", error);
|
|
alert("Network error. Please try again.");
|
|
}
|
|
});
|
|
|
|
async function stopSleep() {
|
|
if (!activeSleep) return;
|
|
|
|
try {
|
|
const now = new Date();
|
|
const localDateTime = new Date(
|
|
now.getTime() - now.getTimezoneOffset() * 60000,
|
|
)
|
|
.toISOString()
|
|
.slice(0, 19);
|
|
|
|
const response = await fetch(`/api/sleep/${activeSleep.id}`, {
|
|
method: "PUT",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
body: JSON.stringify({
|
|
end_time: localDateTime,
|
|
}),
|
|
});
|
|
|
|
if (response.ok) {
|
|
activeSleep = null;
|
|
if (sleepUpdateTimerInterval) {
|
|
clearInterval(sleepUpdateTimerInterval);
|
|
sleepUpdateTimerInterval = null;
|
|
}
|
|
renderContent();
|
|
} else {
|
|
const data = await response.json();
|
|
alert(data.detail || "Failed to stop sleep");
|
|
}
|
|
} catch (error) {
|
|
console.error("Error stopping sleep:", error);
|
|
alert("Network error. Please try again.");
|
|
}
|
|
}
|
|
|
|
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";
|
|
}
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|