Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a966d0bc8e | ||
|
|
753339c28a |
@@ -143,6 +143,12 @@ def serve_menu_js() -> FileResponse:
|
|||||||
return FileResponse(static_path / "menu.js")
|
return FileResponse(static_path / "menu.js")
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/auth-utils.js", include_in_schema=False)
|
||||||
|
def serve_auth_utils_js() -> FileResponse:
|
||||||
|
"""Serve the shared auth utilities JavaScript."""
|
||||||
|
return FileResponse(static_path / "auth-utils.js")
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/")
|
@app.get("/api/")
|
||||||
def read_root(token: Annotated[str, Depends(verify_token)]) -> dict:
|
def read_root(token: Annotated[str, Depends(verify_token)]) -> dict:
|
||||||
"""API root endpoint (requires authentication)."""
|
"""API root endpoint (requires authentication)."""
|
||||||
|
|||||||
@@ -211,12 +211,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script src="/auth-utils.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// Check if user is authenticated
|
// Check if user is authenticated
|
||||||
const token = localStorage.getItem("access_token");
|
const token = getToken();
|
||||||
if (!token) {
|
|
||||||
window.location.href = "/login.html";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get child ID from URL if editing
|
// Get child ID from URL if editing
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
|||||||
@@ -424,35 +424,33 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script src="/auth-utils.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// Check if user is authenticated and is admin
|
// Check if user is authenticated and is admin
|
||||||
const token = localStorage.getItem("access_token");
|
const token = getToken();
|
||||||
if (!token) {
|
|
||||||
window.location.href = "/login.html";
|
|
||||||
}
|
|
||||||
|
|
||||||
let allUsers = [];
|
let allUsers = [];
|
||||||
let userToDelete = null;
|
let userToDelete = null;
|
||||||
|
|
||||||
// Verify user is admin and load data
|
// Load data
|
||||||
fetch("/api/me", {
|
fetch("/api/me", {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then((user) => {
|
.then((user) => {
|
||||||
if (!user.is_admin) {
|
if (!user.is_admin) {
|
||||||
alert("Access denied. Admin privileges required.");
|
alert("Access denied. Admin privileges required.");
|
||||||
window.location.href = "/";
|
window.location.href = "/";
|
||||||
}
|
}
|
||||||
// Load users after verifying admin status
|
// Load users after verifying admin status
|
||||||
loadUsers();
|
loadUsers();
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error("Error verifying admin status:", error);
|
console.error("Error verifying admin status:", error);
|
||||||
window.location.href = "/login.html";
|
window.location.href = "/login.html";
|
||||||
});
|
});
|
||||||
|
|
||||||
async function loadUsers() {
|
async function loadUsers() {
|
||||||
const container = document.getElementById("userListContainer");
|
const container = document.getElementById("userListContainer");
|
||||||
@@ -506,8 +504,8 @@
|
|||||||
ID: ${user.id} • Created: ${new Date(user.created_at).toLocaleDateString()}
|
ID: ${user.id} • Created: ${new Date(user.created_at).toLocaleDateString()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
class="delete-user-btn"
|
class="delete-user-btn"
|
||||||
onclick="showDeleteModal(${user.id}, '${user.username}', ${user.is_admin})"
|
onclick="showDeleteModal(${user.id}, '${user.username}', ${user.is_admin})"
|
||||||
${user.is_admin ? 'disabled title="Cannot delete admin users"' : ""}
|
${user.is_admin ? 'disabled title="Cannot delete admin users"' : ""}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
/**
|
||||||
|
* Redirect to login page with return URL
|
||||||
|
* @param {string} returnUrl - The URL to return to after login (defaults to current page)
|
||||||
|
*/
|
||||||
|
function redirectToLogin(returnUrl) {
|
||||||
|
const url = returnUrl || window.location.pathname + window.location.search;
|
||||||
|
window.location.href = `/login.html?return=${encodeURIComponent(url)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if user has valid token, redirect to login if not
|
||||||
|
* @returns {string|null} - Returns token if valid, redirects to login if not
|
||||||
|
*/
|
||||||
|
function getToken() {
|
||||||
|
const token = localStorage.getItem("access_token");
|
||||||
|
if (!token) {
|
||||||
|
redirectToLogin();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return token;
|
||||||
|
}
|
||||||
@@ -266,12 +266,10 @@
|
|||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js"></script>
|
||||||
<script src="/menu.js"></script>
|
<script src="/menu.js"></script>
|
||||||
|
<script src="/auth-utils.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// Check if user is authenticated
|
// Check if user is authenticated
|
||||||
const token = localStorage.getItem("access_token");
|
const token = getToken();
|
||||||
if (!token) {
|
|
||||||
window.location.href = "/login.html";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize burger menu
|
// Initialize burger menu
|
||||||
initBurgerMenu({ includeHome: true });
|
initBurgerMenu({ includeHome: true });
|
||||||
@@ -281,9 +279,6 @@
|
|||||||
let currentDaysRange =
|
let currentDaysRange =
|
||||||
parseInt(localStorage.getItem("lastDiaperTimeRange")) || 7;
|
parseInt(localStorage.getItem("lastDiaperTimeRange")) || 7;
|
||||||
let selectedChildId = null; // null means show first/only child
|
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
|
// Restore last selected child from localStorage
|
||||||
const lastChildId = localStorage.getItem("lastDiaperChildFilter");
|
const lastChildId = localStorage.getItem("lastDiaperChildFilter");
|
||||||
@@ -785,12 +780,7 @@
|
|||||||
function renderBarChart(chartData) {
|
function renderBarChart(chartData) {
|
||||||
const ctx = document.getElementById("diaperChart").getContext("2d");
|
const ctx = document.getElementById("diaperChart").getContext("2d");
|
||||||
|
|
||||||
// Destroy existing chart instance if it exists
|
new Chart(ctx, {
|
||||||
if (barChartInstance) {
|
|
||||||
barChartInstance.destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
barChartInstance = new Chart(ctx, {
|
|
||||||
type: "bar",
|
type: "bar",
|
||||||
data: {
|
data: {
|
||||||
labels: chartData.labels,
|
labels: chartData.labels,
|
||||||
@@ -883,11 +873,6 @@
|
|||||||
function renderPoopPieChart(chartData) {
|
function renderPoopPieChart(chartData) {
|
||||||
const ctx = document.getElementById("poopPieChart").getContext("2d");
|
const ctx = document.getElementById("poopPieChart").getContext("2d");
|
||||||
|
|
||||||
// Destroy existing chart instance if it exists
|
|
||||||
if (poopPieChartInstance) {
|
|
||||||
poopPieChartInstance.destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
const colors = {
|
const colors = {
|
||||||
Light: {
|
Light: {
|
||||||
bg: "rgba(139, 69, 19, 0.5)",
|
bg: "rgba(139, 69, 19, 0.5)",
|
||||||
@@ -914,7 +899,7 @@
|
|||||||
(label) => colors[label]?.border || "rgba(200, 200, 200, 1)",
|
(label) => colors[label]?.border || "rgba(200, 200, 200, 1)",
|
||||||
);
|
);
|
||||||
|
|
||||||
poopPieChartInstance = new Chart(ctx, {
|
new Chart(ctx, {
|
||||||
type: "pie",
|
type: "pie",
|
||||||
data: {
|
data: {
|
||||||
labels: chartData.labels,
|
labels: chartData.labels,
|
||||||
@@ -970,11 +955,6 @@
|
|||||||
function renderPeePieChart(chartData) {
|
function renderPeePieChart(chartData) {
|
||||||
const ctx = document.getElementById("peePieChart").getContext("2d");
|
const ctx = document.getElementById("peePieChart").getContext("2d");
|
||||||
|
|
||||||
// Destroy existing chart instance if it exists
|
|
||||||
if (peePieChartInstance) {
|
|
||||||
peePieChartInstance.destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
const colors = {
|
const colors = {
|
||||||
Light: {
|
Light: {
|
||||||
bg: "rgba(255, 215, 0, 0.5)",
|
bg: "rgba(255, 215, 0, 0.5)",
|
||||||
@@ -1001,7 +981,7 @@
|
|||||||
(label) => colors[label]?.border || "rgba(200, 200, 200, 1)",
|
(label) => colors[label]?.border || "rgba(200, 200, 200, 1)",
|
||||||
);
|
);
|
||||||
|
|
||||||
peePieChartInstance = new Chart(ctx, {
|
new Chart(ctx, {
|
||||||
type: "pie",
|
type: "pie",
|
||||||
data: {
|
data: {
|
||||||
labels: chartData.labels,
|
labels: chartData.labels,
|
||||||
|
|||||||
@@ -303,12 +303,10 @@
|
|||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js"></script>
|
||||||
<script src="/menu.js"></script>
|
<script src="/menu.js"></script>
|
||||||
|
<script src="/auth-utils.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// Check if user is authenticated
|
// Check if user is authenticated
|
||||||
const token = localStorage.getItem("access_token");
|
const token = getToken();
|
||||||
if (!token) {
|
|
||||||
window.location.href = "/login.html";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize burger menu
|
// Initialize burger menu
|
||||||
initBurgerMenu({ includeHome: true });
|
initBurgerMenu({ includeHome: true });
|
||||||
@@ -319,8 +317,6 @@
|
|||||||
parseInt(localStorage.getItem("lastFeedingTimeRange")) || 7;
|
parseInt(localStorage.getItem("lastFeedingTimeRange")) || 7;
|
||||||
let selectedChildId = null; // null means show first/only child
|
let selectedChildId = null; // null means show first/only child
|
||||||
let barChartInstance = null;
|
let barChartInstance = null;
|
||||||
let pieChartInstance = null;
|
|
||||||
let durationChartInstance = null;
|
|
||||||
|
|
||||||
// Restore last selected child from localStorage
|
// Restore last selected child from localStorage
|
||||||
const lastChildId = localStorage.getItem("lastFeedingChildFilter");
|
const lastChildId = localStorage.getItem("lastFeedingChildFilter");
|
||||||
@@ -981,11 +977,6 @@
|
|||||||
function renderPieChart(chartData) {
|
function renderPieChart(chartData) {
|
||||||
const ctx = document.getElementById("pieChart").getContext("2d");
|
const ctx = document.getElementById("pieChart").getContext("2d");
|
||||||
|
|
||||||
// Destroy existing chart instance if it exists
|
|
||||||
if (pieChartInstance) {
|
|
||||||
pieChartInstance.destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Color mapping for each type
|
// Color mapping for each type
|
||||||
const colorMap = {
|
const colorMap = {
|
||||||
"Left Breast": {
|
"Left Breast": {
|
||||||
@@ -1010,7 +1001,7 @@
|
|||||||
(label) => colorMap[label]?.border || "rgba(200, 200, 200, 1)",
|
(label) => colorMap[label]?.border || "rgba(200, 200, 200, 1)",
|
||||||
);
|
);
|
||||||
|
|
||||||
pieChartInstance = new Chart(ctx, {
|
new Chart(ctx, {
|
||||||
type: "pie",
|
type: "pie",
|
||||||
data: {
|
data: {
|
||||||
labels: chartData.labels,
|
labels: chartData.labels,
|
||||||
@@ -1066,12 +1057,7 @@
|
|||||||
function renderDurationChart(chartData) {
|
function renderDurationChart(chartData) {
|
||||||
const ctx = document.getElementById("durationChart").getContext("2d");
|
const ctx = document.getElementById("durationChart").getContext("2d");
|
||||||
|
|
||||||
// Destroy existing chart instance if it exists
|
new Chart(ctx, {
|
||||||
if (durationChartInstance) {
|
|
||||||
durationChartInstance.destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
durationChartInstance = new Chart(ctx, {
|
|
||||||
type: "bar",
|
type: "bar",
|
||||||
data: {
|
data: {
|
||||||
labels: chartData.labels,
|
labels: chartData.labels,
|
||||||
|
|||||||
@@ -167,52 +167,6 @@
|
|||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
opacity: 0.9;
|
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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -282,59 +236,47 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/menu.js"></script>
|
<script src="/menu.js"></script>
|
||||||
|
<script src="/auth-utils.js"></script>
|
||||||
<script>
|
<script>
|
||||||
const token = localStorage.getItem("access_token");
|
const token = getToken();
|
||||||
const username = localStorage.getItem("username");
|
const username = localStorage.getItem("username");
|
||||||
|
|
||||||
// Check if user is logged in
|
// Fetch current user info including admin status
|
||||||
if (!token) {
|
fetch("/api/me", {
|
||||||
window.location.href = "/login.html";
|
headers: {
|
||||||
} else {
|
Authorization: `Bearer ${token}`,
|
||||||
// Fetch current user info including admin status
|
},
|
||||||
fetch("/api/me", {
|
})
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((user) => {
|
||||||
|
if (!user) return; // Early exit if redirected
|
||||||
|
|
||||||
|
// Initialize burger menu
|
||||||
|
initBurgerMenu({
|
||||||
|
includeHome: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if user has any children
|
||||||
|
return fetch("/api/children", {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => response.json())
|
||||||
if (response.ok) {
|
.then((children) => {
|
||||||
return response.json();
|
// If no children, show welcome message with option to add
|
||||||
} else {
|
if (children.length === 0) {
|
||||||
// Token invalid, redirect to login
|
showNoChildrenMessage();
|
||||||
localStorage.removeItem("access_token");
|
return;
|
||||||
localStorage.removeItem("username");
|
|
||||||
window.location.href = "/login.html";
|
|
||||||
throw new Error("Authentication failed");
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.then((user) => {
|
|
||||||
// Initialize burger menu
|
|
||||||
initBurgerMenu({
|
|
||||||
includeHome: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check if user has any children
|
// Load feeding status and show content
|
||||||
return fetch("/api/children", {
|
loadFeedingStatus(children);
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then((response) => response.json())
|
|
||||||
.then((children) => {
|
|
||||||
// If no children, show welcome message with option to add
|
|
||||||
if (children.length === 0) {
|
|
||||||
showNoChildrenMessage();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load feeding status and show content
|
|
||||||
loadFeedingStatus(children);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("Error:", error);
|
|
||||||
});
|
});
|
||||||
}
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Error:", error);
|
||||||
|
});
|
||||||
|
|
||||||
function showNoChildrenMessage() {
|
function showNoChildrenMessage() {
|
||||||
const content = document.getElementById("content");
|
const content = document.getElementById("content");
|
||||||
@@ -367,7 +309,6 @@
|
|||||||
let activeSleep = null;
|
let activeSleep = null;
|
||||||
let lastSleep = null;
|
let lastSleep = null;
|
||||||
let sleepUpdateTimerInterval = null;
|
let sleepUpdateTimerInterval = null;
|
||||||
let lastDiaperChange = null;
|
|
||||||
|
|
||||||
async function loadFeedingStatus(children) {
|
async function loadFeedingStatus(children) {
|
||||||
userChildren = children;
|
userChildren = children;
|
||||||
@@ -384,8 +325,8 @@
|
|||||||
activeFeeding = await activeResponse.json();
|
activeFeeding = await activeResponse.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch the 10 most recent feedings to calculate the most recent session
|
// Fetch all feedings to get the last one
|
||||||
const feedingsResponse = await fetch("/api/feedings?limit=10", {
|
const feedingsResponse = await fetch("/api/feedings", {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
},
|
},
|
||||||
@@ -394,68 +335,8 @@
|
|||||||
if (feedingsResponse.ok) {
|
if (feedingsResponse.ok) {
|
||||||
const feedings = await feedingsResponse.json();
|
const feedings = await feedingsResponse.json();
|
||||||
if (feedings.length > 0) {
|
if (feedings.length > 0) {
|
||||||
// Calculate the most recent feeding session
|
// Feedings are ordered by start_time desc, so first is most recent
|
||||||
let sessionStart = feedings[0].start_time;
|
lastFeeding = feedings[0];
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -483,20 +364,6 @@
|
|||||||
lastSleep = sleeps[0];
|
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) {
|
} catch (error) {
|
||||||
console.error("Error loading feeding status:", error);
|
console.error("Error loading feeding status:", error);
|
||||||
}
|
}
|
||||||
@@ -570,7 +437,7 @@
|
|||||||
</button>
|
</button>
|
||||||
`
|
`
|
||||||
: `
|
: `
|
||||||
<button class="feeding-button" onclick="window.location.href='/log-feeding.html'">
|
<button class="feeding-button" onclick="showStartFeedingModal()">
|
||||||
🍼 Started Feeding
|
🍼 Started Feeding
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
@@ -599,100 +466,9 @@
|
|||||||
</button>
|
</button>
|
||||||
${feedingButtonHtml}
|
${feedingButtonHtml}
|
||||||
${sleepButtonHtml}
|
${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) {
|
function formatFeedingType(type) {
|
||||||
const types = {
|
const types = {
|
||||||
left_breast: "Left Breast",
|
left_breast: "Left Breast",
|
||||||
@@ -864,7 +640,42 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function showStartSleepModal() {
|
function showStartSleepModal() {
|
||||||
window.location.href = "/log-sleep.html";
|
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");
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeStartSleepModal() {
|
function closeStartSleepModal() {
|
||||||
|
|||||||
@@ -242,12 +242,10 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script src="/auth-utils.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// Check if user is authenticated
|
// Check if user is authenticated
|
||||||
const token = localStorage.getItem("access_token");
|
const token = getToken();
|
||||||
if (!token) {
|
|
||||||
window.location.href = "/login.html";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine return URL based on referrer
|
// Determine return URL based on referrer
|
||||||
function getReturnUrl() {
|
function getReturnUrl() {
|
||||||
@@ -336,29 +334,6 @@
|
|||||||
lastDiaperChange.child_id;
|
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 {
|
} else {
|
||||||
showMessage("Failed to load children", "error");
|
showMessage("Failed to load children", "error");
|
||||||
|
|||||||
@@ -199,12 +199,10 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script src="/auth-utils.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// Check if user is authenticated
|
// Check if user is authenticated
|
||||||
const token = localStorage.getItem("access_token");
|
const token = getToken();
|
||||||
if (!token) {
|
|
||||||
window.location.href = "/login.html";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine return URL based on referrer
|
// Determine return URL based on referrer
|
||||||
function getReturnUrl() {
|
function getReturnUrl() {
|
||||||
@@ -303,22 +301,6 @@
|
|||||||
// If editing, load the feeding data
|
// If editing, load the feeding data
|
||||||
if (isEditMode) {
|
if (isEditMode) {
|
||||||
loadFeedingData();
|
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 {
|
} else {
|
||||||
showMessage("Failed to load children", "error");
|
showMessage("Failed to load children", "error");
|
||||||
|
|||||||
@@ -189,12 +189,10 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script src="/auth-utils.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// Check if user is authenticated
|
// Check if user is authenticated
|
||||||
const token = localStorage.getItem("access_token");
|
const token = getToken();
|
||||||
if (!token) {
|
|
||||||
window.location.href = "/login.html";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine return URL based on referrer
|
// Determine return URL based on referrer
|
||||||
function getReturnUrl() {
|
function getReturnUrl() {
|
||||||
|
|||||||
@@ -82,10 +82,36 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
// Check if redirected due to expired session
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const returnUrl = params.get("return");
|
||||||
|
|
||||||
const form = document.getElementById("loginForm");
|
const form = document.getElementById("loginForm");
|
||||||
const messageDiv = document.getElementById("message");
|
const messageDiv = document.getElementById("message");
|
||||||
const loginButton = document.getElementById("loginButton");
|
const loginButton = document.getElementById("loginButton");
|
||||||
|
|
||||||
|
// Show session expired message if redirected from another page
|
||||||
|
if (returnUrl) {
|
||||||
|
showMessage("Your session has expired. Please log in again.", "error");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear user-specific cache data on login
|
||||||
|
function clearUserCache() {
|
||||||
|
const keysToRemove = [
|
||||||
|
'lastDiaperChildFilter',
|
||||||
|
'lastFeedingChildFilter',
|
||||||
|
'lastSleepChildFilter',
|
||||||
|
'lastDiaperTimeRange',
|
||||||
|
'lastFeedingTimeRange'
|
||||||
|
];
|
||||||
|
|
||||||
|
keysToRemove.forEach(key => {
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Cleared user cache on login');
|
||||||
|
}
|
||||||
|
|
||||||
form.addEventListener("submit", async (e) => {
|
form.addEventListener("submit", async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
@@ -109,13 +135,23 @@
|
|||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
showMessage(data.message, "success");
|
showMessage(data.message, "success");
|
||||||
|
|
||||||
// Store token in localStorage
|
// Store token in localStorage
|
||||||
localStorage.setItem("access_token", data.access_token);
|
localStorage.setItem("access_token", data.access_token);
|
||||||
localStorage.setItem("username", data.username);
|
localStorage.setItem("username", data.username);
|
||||||
|
|
||||||
|
// Clear stale user preferences from previous sessions
|
||||||
|
clearUserCache();
|
||||||
|
|
||||||
// Redirect based on is_admin from login response
|
// Check for return URL parameter
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const returnUrl = params.get("return");
|
||||||
|
|
||||||
|
// Redirect based on return URL or is_admin from login response
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (data.is_admin === true) {
|
if (returnUrl) {
|
||||||
|
window.location.href = returnUrl;
|
||||||
|
} else if (data.is_admin === true) {
|
||||||
window.location.href = "/admin.html";
|
window.location.href = "/admin.html";
|
||||||
} else {
|
} else {
|
||||||
window.location.href = "/";
|
window.location.href = "/";
|
||||||
|
|||||||
@@ -469,11 +469,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/menu.js"></script>
|
<script src="/menu.js"></script>
|
||||||
|
<script src="/auth-utils.js"></script>
|
||||||
<script>
|
<script>
|
||||||
const token = localStorage.getItem("access_token");
|
const token = getToken();
|
||||||
if (!token) {
|
|
||||||
window.location.href = "/login.html";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize burger menu
|
// Initialize burger menu
|
||||||
initBurgerMenu({ includeHome: true });
|
initBurgerMenu({ includeHome: true });
|
||||||
@@ -495,7 +493,7 @@
|
|||||||
currentUser = await userResponse.json();
|
currentUser = await userResponse.json();
|
||||||
document.getElementById("username").value = currentUser.username;
|
document.getElementById("username").value = currentUser.username;
|
||||||
} else {
|
} else {
|
||||||
window.location.href = "/login.html";
|
redirectToLogin();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -852,8 +850,10 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Load data on page load
|
// Load data on page load (only if authenticated)
|
||||||
loadData();
|
if (token) {
|
||||||
|
loadData();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -313,13 +313,9 @@
|
|||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js"></script>
|
||||||
<script src="/menu.js"></script>
|
<script src="/menu.js"></script>
|
||||||
|
<script src="/auth-utils.js"></script>
|
||||||
<script>
|
<script>
|
||||||
const token = localStorage.getItem("access_token");
|
const token = getToken();
|
||||||
|
|
||||||
// Check if user is logged in
|
|
||||||
if (!token) {
|
|
||||||
window.location.href = "/login.html";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize burger menu
|
// Initialize burger menu
|
||||||
initBurgerMenu({ includeHome: true });
|
initBurgerMenu({ includeHome: true });
|
||||||
@@ -390,7 +386,7 @@
|
|||||||
readonly
|
readonly
|
||||||
style="background-color: #f5f5f5; cursor: default; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;"
|
style="background-color: #f5f5f5; cursor: default; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<label for="timeRangeFilter">Time Range:</label>
|
<label for="timeRangeFilter">Time Range:</label>
|
||||||
<select id="timeRangeFilter">
|
<select id="timeRangeFilter">
|
||||||
<option value="1">Last 24 hours</option>
|
<option value="1">Last 24 hours</option>
|
||||||
@@ -1104,9 +1100,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="sleep-duration">${durationText}</div>
|
<div class="sleep-duration">${durationText}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="sleep-actions">
|
|
||||||
<button class="edit-button" onclick="window.location.href='/log-sleep.html?id=${sleep.id}'">Edit</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user