Merge pull request 'Show who children are shared with' (#30) from show-who-children-are-shared-with into main
Reviewed-on: #30
This commit was merged in pull request #30.
This commit is contained in:
@@ -20,3 +20,5 @@ class ChildResponse(BaseModel):
|
|||||||
birth_time: datetime
|
birth_time: datetime
|
||||||
birth_weight: float
|
birth_weight: float
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
|
parent_count: int | None = None
|
||||||
|
parent_usernames: list[str] | None = None
|
||||||
|
|||||||
@@ -11,10 +11,12 @@ from baby_monitor.routers.auth import verify_token
|
|||||||
from baby_monitor.repositories.interfaces import (
|
from baby_monitor.repositories.interfaces import (
|
||||||
ChildRepositoryInterface,
|
ChildRepositoryInterface,
|
||||||
ChildInvitationRepositoryInterface,
|
ChildInvitationRepositoryInterface,
|
||||||
|
UserRepositoryInterface,
|
||||||
)
|
)
|
||||||
from baby_monitor.repositories.dependencies import (
|
from baby_monitor.repositories.dependencies import (
|
||||||
get_child_repository,
|
get_child_repository,
|
||||||
get_child_invitation_repository,
|
get_child_invitation_repository,
|
||||||
|
get_user_repository,
|
||||||
)
|
)
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/children", tags=["children"])
|
router = APIRouter(prefix="/api/children", tags=["children"])
|
||||||
@@ -62,10 +64,29 @@ def create_child(
|
|||||||
def get_user_children(
|
def get_user_children(
|
||||||
user_id: Annotated[int, Depends(verify_token)],
|
user_id: Annotated[int, Depends(verify_token)],
|
||||||
child_repo: Annotated[ChildRepositoryInterface, Depends(get_child_repository)],
|
child_repo: Annotated[ChildRepositoryInterface, Depends(get_child_repository)],
|
||||||
|
user_repo: Annotated[UserRepositoryInterface, Depends(get_user_repository)],
|
||||||
) -> list[ChildResponse]:
|
) -> list[ChildResponse]:
|
||||||
"""Get all children for the authenticated user."""
|
"""Get all children for the authenticated user."""
|
||||||
children = child_repo.get_by_user_id(user_id)
|
children = child_repo.get_by_user_id(user_id)
|
||||||
return [ChildResponse(**child) for child in children]
|
|
||||||
|
# Add parent information to each child
|
||||||
|
result = []
|
||||||
|
for child in children:
|
||||||
|
child_data = child.copy()
|
||||||
|
parent_ids = child_repo.get_parent_ids(child["id"])
|
||||||
|
child_data["parent_count"] = len(parent_ids)
|
||||||
|
|
||||||
|
# Get usernames of all parents
|
||||||
|
parent_usernames = []
|
||||||
|
for parent_id in parent_ids:
|
||||||
|
parent = user_repo.get_by_id(parent_id)
|
||||||
|
if parent:
|
||||||
|
parent_usernames.append(parent["username"])
|
||||||
|
child_data["parent_usernames"] = parent_usernames
|
||||||
|
|
||||||
|
result.append(ChildResponse(**child_data))
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{child_id}", response_model=ChildResponse)
|
@router.get("/{child_id}", response_model=ChildResponse)
|
||||||
|
|||||||
@@ -371,7 +371,9 @@
|
|||||||
|
|
||||||
content.innerHTML = `
|
content.innerHTML = `
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
${allChildren.length === 1 ? `
|
${
|
||||||
|
allChildren.length === 1
|
||||||
|
? `
|
||||||
<label for="childDisplay">Child:</label>
|
<label for="childDisplay">Child:</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -380,12 +382,14 @@
|
|||||||
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="childFilter">Child:</label>
|
<label for="childFilter">Child:</label>
|
||||||
<select id="childFilter" onchange="onChildChange(event)">
|
<select id="childFilter" onchange="onChildChange(event)">
|
||||||
${childOptions}
|
${childOptions}
|
||||||
</select>
|
</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)">
|
||||||
|
|||||||
@@ -409,7 +409,9 @@
|
|||||||
|
|
||||||
content.innerHTML = `
|
content.innerHTML = `
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
${allChildren.length === 1 ? `
|
${
|
||||||
|
allChildren.length === 1
|
||||||
|
? `
|
||||||
<label for="childDisplay">Child:</label>
|
<label for="childDisplay">Child:</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -418,12 +420,14 @@
|
|||||||
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="childFilter">Child:</label>
|
<label for="childFilter">Child:</label>
|
||||||
<select id="childFilter" onchange="onChildChange(event)">
|
<select id="childFilter" onchange="onChildChange(event)">
|
||||||
${childOptions}
|
${childOptions}
|
||||||
</select>
|
</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)">
|
||||||
|
|||||||
@@ -252,11 +252,11 @@
|
|||||||
// Determine return URL based on referrer
|
// Determine return URL based on referrer
|
||||||
function getReturnUrl() {
|
function getReturnUrl() {
|
||||||
const referrer = document.referrer;
|
const referrer = document.referrer;
|
||||||
if (referrer.includes('/diapers.html')) {
|
if (referrer.includes("/diapers.html")) {
|
||||||
return '/diapers.html';
|
return "/diapers.html";
|
||||||
}
|
}
|
||||||
// Default to home page
|
// Default to home page
|
||||||
return '/';
|
return "/";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we're in edit mode
|
// Check if we're in edit mode
|
||||||
@@ -305,7 +305,7 @@
|
|||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
// Multiple children: show dropdown (clear the placeholder first)
|
// Multiple children: show dropdown (clear the placeholder first)
|
||||||
select.innerHTML = '';
|
select.innerHTML = "";
|
||||||
children.forEach((child) => {
|
children.forEach((child) => {
|
||||||
const option = document.createElement("option");
|
const option = document.createElement("option");
|
||||||
option.value = child.id;
|
option.value = child.id;
|
||||||
|
|||||||
@@ -209,11 +209,11 @@
|
|||||||
// Determine return URL based on referrer
|
// Determine return URL based on referrer
|
||||||
function getReturnUrl() {
|
function getReturnUrl() {
|
||||||
const referrer = document.referrer;
|
const referrer = document.referrer;
|
||||||
if (referrer.includes('/feedings.html')) {
|
if (referrer.includes("/feedings.html")) {
|
||||||
return '/feedings.html';
|
return "/feedings.html";
|
||||||
}
|
}
|
||||||
// Default to home page
|
// Default to home page
|
||||||
return '/';
|
return "/";
|
||||||
}
|
}
|
||||||
|
|
||||||
const form = document.getElementById("logFeedingForm");
|
const form = document.getElementById("logFeedingForm");
|
||||||
@@ -287,7 +287,7 @@
|
|||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
// Multiple children: show dropdown (clear placeholder first)
|
// Multiple children: show dropdown (clear placeholder first)
|
||||||
childSelect.innerHTML = '';
|
childSelect.innerHTML = "";
|
||||||
children.forEach((child) => {
|
children.forEach((child) => {
|
||||||
const option = document.createElement("option");
|
const option = document.createElement("option");
|
||||||
option.value = child.id;
|
option.value = child.id;
|
||||||
|
|||||||
@@ -199,11 +199,11 @@
|
|||||||
// Determine return URL based on referrer
|
// Determine return URL based on referrer
|
||||||
function getReturnUrl() {
|
function getReturnUrl() {
|
||||||
const referrer = document.referrer;
|
const referrer = document.referrer;
|
||||||
if (referrer.includes('/sleep.html')) {
|
if (referrer.includes("/sleep.html")) {
|
||||||
return '/sleep.html';
|
return "/sleep.html";
|
||||||
}
|
}
|
||||||
// Default to home page
|
// Default to home page
|
||||||
return '/';
|
return "/";
|
||||||
}
|
}
|
||||||
|
|
||||||
const form = document.getElementById("logSleepForm");
|
const form = document.getElementById("logSleepForm");
|
||||||
@@ -277,7 +277,7 @@
|
|||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
// Multiple children: show dropdown (clear placeholder first)
|
// Multiple children: show dropdown (clear placeholder first)
|
||||||
childSelect.innerHTML = '';
|
childSelect.innerHTML = "";
|
||||||
children.forEach((child) => {
|
children.forEach((child) => {
|
||||||
const option = document.createElement("option");
|
const option = document.createElement("option");
|
||||||
option.value = child.id;
|
option.value = child.id;
|
||||||
|
|||||||
@@ -166,6 +166,16 @@
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.shared-badge {
|
||||||
|
display: inline-block;
|
||||||
|
background: #4caf50;
|
||||||
|
color: white;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
.loading {
|
.loading {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 40px;
|
padding: 40px;
|
||||||
@@ -468,6 +478,75 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit Child Modal -->
|
||||||
|
<div
|
||||||
|
class="modal"
|
||||||
|
id="editChildModal"
|
||||||
|
style="
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 1000;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style="
|
||||||
|
background: white;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 12px;
|
||||||
|
max-width: 500px;
|
||||||
|
width: 90%;
|
||||||
|
max-height: 80vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<h2 style="margin-top: 0">✏️ Edit Child</h2>
|
||||||
|
<div style="margin-bottom: 20px">
|
||||||
|
<h3 style="color: #333; margin-bottom: 10px" id="editChildName"></h3>
|
||||||
|
<div style="color: #666; font-size: 14px">
|
||||||
|
<p>
|
||||||
|
<strong>Birth Date:</strong> <span id="editChildBirthDate"></span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Birth Weight:</strong>
|
||||||
|
<span id="editChildBirthWeight"></span>g
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="sharedUsersSection" style="margin-bottom: 20px; display: none">
|
||||||
|
<h3 style="color: #333; font-size: 16px; margin-bottom: 10px">
|
||||||
|
👥 Shared With
|
||||||
|
</h3>
|
||||||
|
<div
|
||||||
|
id="sharedUsersList"
|
||||||
|
style="
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="button"
|
||||||
|
onclick="closeEditChildModal()"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="/menu.js"></script>
|
<script src="/menu.js"></script>
|
||||||
<script>
|
<script>
|
||||||
const token = localStorage.getItem("access_token");
|
const token = localStorage.getItem("access_token");
|
||||||
@@ -536,7 +615,10 @@
|
|||||||
.map(
|
.map(
|
||||||
(child) => `
|
(child) => `
|
||||||
<div class="child-card">
|
<div class="child-card">
|
||||||
<h3>👶 ${child.name}</h3>
|
<div style="display: flex; align-items: center; gap: 10px;">
|
||||||
|
<h3 style="margin: 0;">👶 ${child.name}</h3>
|
||||||
|
${child.parent_count > 1 ? '<span class="shared-badge">Shared</span>' : ""}
|
||||||
|
</div>
|
||||||
<div class="child-info">
|
<div class="child-info">
|
||||||
<p><strong>Birth Date:</strong> ${formatDate(child.birth_time)}</p>
|
<p><strong>Birth Date:</strong> ${formatDate(child.birth_time)}</p>
|
||||||
<p><strong>Birth Weight:</strong> ${child.birth_weight}g</p>
|
<p><strong>Birth Weight:</strong> ${child.birth_weight}g</p>
|
||||||
@@ -568,12 +650,49 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function editChild(childId) {
|
function editChild(childId) {
|
||||||
// Redirect to a child edit page or show a modal
|
|
||||||
// For now, just show an alert
|
|
||||||
const child = children.find((c) => c.id === childId);
|
const child = children.find((c) => c.id === childId);
|
||||||
if (child) {
|
if (!child) return;
|
||||||
alert(`Edit functionality for ${child.name} coming soon!`);
|
|
||||||
|
// Populate modal with child information
|
||||||
|
document.getElementById("editChildName").textContent =
|
||||||
|
`👶 ${child.name}`;
|
||||||
|
document.getElementById("editChildBirthDate").textContent = formatDate(
|
||||||
|
child.birth_time,
|
||||||
|
);
|
||||||
|
document.getElementById("editChildBirthWeight").textContent =
|
||||||
|
child.birth_weight;
|
||||||
|
|
||||||
|
// Show shared users if child is shared
|
||||||
|
const sharedSection = document.getElementById("sharedUsersSection");
|
||||||
|
const sharedList = document.getElementById("sharedUsersList");
|
||||||
|
|
||||||
|
if (child.parent_usernames && child.parent_usernames.length > 1) {
|
||||||
|
// Filter out current user and show other users
|
||||||
|
const otherUsers = child.parent_usernames.filter(
|
||||||
|
(username) => username !== currentUser.username,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (otherUsers.length > 0) {
|
||||||
|
sharedSection.style.display = "block";
|
||||||
|
sharedList.innerHTML = otherUsers
|
||||||
|
.map(
|
||||||
|
(username) =>
|
||||||
|
`<div style="padding: 5px 0;">• ${username}</div>`,
|
||||||
|
)
|
||||||
|
.join("");
|
||||||
|
} else {
|
||||||
|
sharedSection.style.display = "none";
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
sharedSection.style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show the modal
|
||||||
|
document.getElementById("editChildModal").style.display = "flex";
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeEditChildModal() {
|
||||||
|
document.getElementById("editChildModal").style.display = "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
async function shareChild(childId) {
|
async function shareChild(childId) {
|
||||||
|
|||||||
@@ -374,7 +374,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Populate child filter or show single child
|
// Populate child filter or show single child
|
||||||
const childFilterContainer = document.querySelector('.controls');
|
const childFilterContainer = document.querySelector(".controls");
|
||||||
if (allChildren.length === 1) {
|
if (allChildren.length === 1) {
|
||||||
// Single child: show as read-only text
|
// Single child: show as read-only text
|
||||||
const child = allChildren[0];
|
const child = allChildren[0];
|
||||||
@@ -401,7 +401,7 @@
|
|||||||
} else {
|
} else {
|
||||||
// Multiple children: show dropdown
|
// Multiple children: show dropdown
|
||||||
const childFilter = document.getElementById("childFilter");
|
const childFilter = document.getElementById("childFilter");
|
||||||
childFilter.innerHTML = '';
|
childFilter.innerHTML = "";
|
||||||
allChildren.forEach((child) => {
|
allChildren.forEach((child) => {
|
||||||
const option = document.createElement("option");
|
const option = document.createElement("option");
|
||||||
option.value = child.id;
|
option.value = child.id;
|
||||||
|
|||||||
Reference in New Issue
Block a user