This commit is contained in:
2025-12-02 17:02:20 -05:00
parent f82ba25160
commit 6423d1c1a2
49 changed files with 2320 additions and 349 deletions

View File

@@ -1,37 +1,161 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { ref, onMounted, onUnmounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import ChildDetailCard from './ChildDetailCard.vue'
import ChildTaskList from '../task/ChildTaskList.vue'
import ChildRewardList from '../reward/ChildRewardList.vue'
interface Child {
id: string | number
name: string
age: number
points?: number
}
import { eventBus } from '@/common/eventBus'
import type {
Child,
Event,
Task,
Reward,
TaskUpdateEventPayload,
RewardUpdateEventPayload,
ChildUpdateEventPayload,
ChildDeleteEventPayload,
TaskCreatedEventPayload,
TaskDeletedEventPayload,
TaskEditedEventPayload,
RewardCreatedEventPayload,
RewardDeletedEventPayload,
RewardEditedEventPayload,
} from '@/common/models'
const route = useRoute()
const router = useRouter()
const child = ref<Child | null>(null)
const tasks = ref<string[]>([])
const rewards = ref<string[]>([])
const loading = ref(true)
const error = ref<string | null>(null)
onMounted(async () => {
function handlePointsUpdate(event: Event) {
const payload = event.payload as TaskUpdateEventPayload | RewardUpdateEventPayload
if (child.value && payload.child_id == child.value.id) {
child.value.points = payload.points
}
}
function handleServerChange(event: Event) {
const payload = event.payload as
| TaskUpdateEventPayload
| RewardUpdateEventPayload
| ChildUpdateEventPayload
if (child.value && payload.child_id == child.value.id) {
fetchChildData(child.value.id)
}
}
function handleChildDeletion(event: Event) {
const payload = event.payload as ChildDeleteEventPayload
if (child.value && payload.child_id == child.value.id) {
// Navigate away back to children list
router.push({ name: 'ChildrenListView' })
}
}
function handleTaskChanged(event: Event) {
const payload = event.payload as
| TaskCreatedEventPayload
| TaskDeletedEventPayload
| TaskEditedEventPayload
if (child.value) {
const task_id = payload.task_id
if (tasks.value.includes(task_id)) {
fetchChildData(child.value.id)
}
}
}
function handleRewardChanged(event: Event) {
const payload = event.payload as
| RewardCreatedEventPayload
| RewardDeletedEventPayload
| RewardEditedEventPayload
if (child.value) {
const reward_id = payload.reward_id
if (rewards.value.includes(reward_id)) {
fetchChildData(child.value.id)
}
}
}
const handleTriggerTask = (task: Task) => {
if ('speechSynthesis' in window && task.name) {
const utter = new window.SpeechSynthesisUtterance(task.name)
window.speechSynthesis.speak(utter)
}
}
const handleTriggerReward = (reward: Reward, redeemable: boolean) => {
if ('speechSynthesis' in window && reward.name) {
console.log('Handle trigger reward:', reward, redeemable)
const utterString =
reward.name + (redeemable ? '' : `, You still need ${reward.points_needed} points.`)
const utter = new window.SpeechSynthesisUtterance(utterString)
window.speechSynthesis.speak(utter)
}
}
async function fetchChildData(id: string | number) {
loading.value = true
try {
const resp = await fetch(`/api/child/${route.params.id}`)
const resp = await fetch(`/api/child/${id}`)
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
const data = await resp.json()
child.value = data.children ? data.children : data
tasks.value = data.tasks || []
rewards.value = data.rewards || []
error.value = null
} catch (err) {
error.value = err instanceof Error ? err.message : 'Failed to fetch child'
console.error(err)
} finally {
loading.value = false
}
}
onMounted(async () => {
try {
eventBus.on('task_update', handlePointsUpdate)
eventBus.on('reward_update', handlePointsUpdate)
eventBus.on('task_set', handleServerChange)
eventBus.on('reward_set', handleServerChange)
eventBus.on('child_update', handleServerChange)
eventBus.on('child_delete', handleChildDeletion)
eventBus.on('task_created', handleTaskChanged)
eventBus.on('task_deleted', handleTaskChanged)
eventBus.on('task_edited', handleTaskChanged)
eventBus.on('reward_created', handleRewardChanged)
eventBus.on('reward_deleted', handleRewardChanged)
eventBus.on('reward_edited', handleRewardChanged)
if (route.params.id) {
const idParam = Array.isArray(route.params.id) ? route.params.id[0] : route.params.id
if (idParam !== undefined) {
fetchChildData(idParam)
}
}
} catch (err) {
console.error('Error in onMounted:', err)
}
})
onUnmounted(() => {
eventBus.off('task_update', handlePointsUpdate)
eventBus.off('reward_update', handlePointsUpdate)
eventBus.off('task_set', handleServerChange)
eventBus.off('reward_set', handleServerChange)
eventBus.off('child_update', handleServerChange)
eventBus.off('child_delete', handleChildDeletion)
eventBus.off('task_created', handleTaskChanged)
eventBus.off('task_deleted', handleTaskChanged)
eventBus.off('task_edited', handleTaskChanged)
eventBus.off('reward_created', handleRewardChanged)
eventBus.off('reward_deleted', handleRewardChanged)
eventBus.off('reward_edited', handleRewardChanged)
})
</script>
@@ -44,26 +168,33 @@ onMounted(async () => {
<div class="main">
<ChildDetailCard :child="child" />
<ChildTaskList
title="Chores"
:task-ids="tasks"
:child-id="child ? child.id : null"
:is-parent-authenticated="false"
:filter-type="1"
@trigger-task="handleTriggerTask"
/>
<ChildTaskList
title="Bad Habits"
:task-ids="tasks"
:child-id="child ? child.id : null"
:is-parent-authenticated="false"
:filter-type="2"
@trigger-task="handleTriggerTask"
/>
<ChildRewardList
:child-id="child ? child.id : null"
:child-points="child?.points ?? 0"
:is-parent-authenticated="false"
@trigger-reward="handleTriggerReward"
@points-updated="
({ id, points }) => {
if (child && child.id === id) child.points = points
}
"
/>
<!-- removed placeholder -->
</div>
<!-- Remove this aside block:
<aside class="side">
<div class="placeholder">Additional components go here</div>
</aside>
-->
</div>
</div>
</template>
@@ -74,7 +205,7 @@ onMounted(async () => {
max-width: 1200px;
margin: 0 auto;
min-height: 100vh;
padding: 2rem;
padding: 0.5rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
box-sizing: border-box;
}