Refactor code structure for improved readability and maintainability
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 3m27s

This commit is contained in:
2026-04-13 10:27:30 -04:00
parent 759a1c745e
commit 876d3c5531
9 changed files with 147 additions and 68 deletions

View File

@@ -301,7 +301,10 @@ function handleChoreConfirmation(event: Event) {
function isChoreCompletedToday(item: ChildTask): boolean {
if (item.pending_status !== 'approved' || !item.approved_at) return false
const today = new Date().toISOString().slice(0, 10)
return item.approved_at.slice(0, 10) === today
if (item.approved_at.slice(0, 10) !== today) return false
// If the task has a schedule and today is not a scheduled day, don't show as completed
if (item.schedule && !isScheduledToday(item.schedule, new Date())) return false
return true
}
function isChorePending(item: ChildTask): boolean {

View File

@@ -1,7 +1,13 @@
<script setup lang="ts">
import { useRouter, useRoute } from 'vue-router'
import { computed, ref, onMounted } from 'vue'
import { computed, ref, onMounted, onUnmounted } from 'vue'
import LoginButton from '../components/shared/LoginButton.vue'
import { eventBus } from '@/common/eventBus'
import type {
Event,
ChildRewardRequestEventPayload,
ChildChoreConfirmationPayload,
} from '@/common/models'
const router = useRouter()
const route = useRoute()
@@ -30,9 +36,42 @@ const showBack = computed(
),
)
// Notification badge count
const notificationCount = ref(0)
async function fetchNotificationCount() {
try {
const resp = await fetch('/api/pending-confirmations')
if (resp.ok) {
const data = await resp.json()
notificationCount.value = Array.isArray(data.confirmations) ? data.confirmations.length : 0
}
} catch {
notificationCount.value = 0
}
}
function handleRewardRequestBadge(event: Event) {
const payload = event.payload as ChildRewardRequestEventPayload
if (['CREATED', 'CANCELLED', 'GRANTED'].includes(payload.operation)) {
fetchNotificationCount()
}
}
function handleChoreConfirmationBadge(event: Event) {
const payload = event.payload as ChildChoreConfirmationPayload
if (['CONFIRMED', 'APPROVED', 'REJECTED', 'CANCELLED'].includes(payload.operation)) {
fetchNotificationCount()
}
}
// Version fetching
const appVersion = ref('')
onMounted(async () => {
await fetchNotificationCount()
eventBus.on('child_reward_request', handleRewardRequestBadge)
eventBus.on('child_chore_confirmation', handleChoreConfirmationBadge)
try {
const resp = await fetch('/api/version')
if (resp.ok) {
@@ -43,6 +82,11 @@ onMounted(async () => {
appVersion.value = ''
}
})
onUnmounted(() => {
eventBus.off('child_reward_request', handleRewardRequestBadge)
eventBus.off('child_chore_confirmation', handleChoreConfirmationBadge)
})
</script>
<template>
@@ -69,22 +113,7 @@ onMounted(async () => {
aria-label="Children"
title="Children"
>
<!-- Children Icon -->
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.7"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="8" cy="10" r="3" />
<circle cx="16" cy="10" r="3" />
<path d="M2 20c0-2.5 3-4.5 6-4.5s6 2 6 4.5" />
<path d="M10 20c0-2 2-3.5 6-3.5s6 1.5 6 3.5" />
</svg>
<img src="/nav/children.png" class="nav-icon" alt="Children" />
</button>
<button
:class="{
@@ -105,21 +134,7 @@ onMounted(async () => {
aria-label="Tasks"
title="Tasks"
>
<!-- Book Icon -->
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.7"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20" />
<path d="M20 22V6a2 2 0 0 0-2-2H6.5A2.5 2.5 0 0 0 4 6.5v13" />
<path d="M16 2v4" />
</svg>
<img src="/nav/chores.png" class="nav-icon" alt="Tasks" />
</button>
<button
:class="{
@@ -129,23 +144,7 @@ onMounted(async () => {
aria-label="Rewards"
title="Rewards"
>
<!-- Trophy Icon -->
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.7"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M8 21h8" />
<path d="M12 17v4" />
<path d="M17 17a5 5 0 0 0 5-5V7h-4" />
<path d="M7 17a5 5 0 0 1-5-5V7h4" />
<rect x="7" y="2" width="10" height="15" rx="5" />
</svg>
<img src="/nav/rewards.png" class="nav-icon" alt="Rewards" />
</button>
<button
:class="{ active: ['NotificationView'].includes(String(route.name)) }"
@@ -153,22 +152,10 @@ onMounted(async () => {
aria-label="Notifications"
title="Notifications"
>
<!-- Notification/Bell Icon -->
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.7"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M18 16v-5a6 6 0 1 0-12 0v5" />
<path d="M2 16h20" />
<path d="M8 20a4 4 0 0 0 8 0" />
<circle cx="19" cy="7" r="2" fill="#ef4444" stroke="none" />
</svg>
<span class="nav-icon-wrap">
<img src="/nav/notifications.png" class="nav-icon" alt="Notifications" />
<span v-if="notificationCount > 0" class="notification-badge"></span>
</span>
</button>
</nav>
<div v-else class="spacer"></div>
@@ -293,8 +280,37 @@ onMounted(async () => {
color: var(--button-active-text);
}
.view-selector button.active svg {
stroke: var(--button-active-text);
.nav-icon {
width: 24px;
height: 24px;
object-fit: contain;
opacity: 0.55;
transition: opacity 0.18s;
display: block;
}
.nav-icon-wrap {
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.notification-badge {
position: absolute;
top: -3px;
right: -3px;
width: 8px;
height: 8px;
background: var(--btn-danger, #ef4444);
border-radius: 50%;
border: 1.5px solid var(--button-bg, #fff);
pointer-events: none;
}
.view-selector button.active .nav-icon,
.view-selector button:hover .nav-icon {
opacity: 1;
}
.view-selector button:hover:not(.active) {