feat: Implement routines feature with CRUD operations and child assignment
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 3m0s
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 3m0s
- Add backend routines management with add, get, update, delete, and list functionalities. - Create models for Routine, RoutineItem, RoutineSchedule, and RoutineExtension. - Develop event types for routine confirmation and modification. - Implement frontend components for routine assignment, confirmation dialog, and routine management views. - Add unit tests for routine API and integration tests for routine CRUD flow. - Create end-to-end test plan for routines feature covering parent and child interactions.
This commit is contained in:
@@ -6,6 +6,7 @@ import ScrollingList from '../shared/ScrollingList.vue'
|
||||
import StatusMessage from '../shared/StatusMessage.vue'
|
||||
import RewardConfirmDialog from './RewardConfirmDialog.vue'
|
||||
import ChoreConfirmDialog from './ChoreConfirmDialog.vue'
|
||||
import RoutineConfirmDialog from './RoutineConfirmDialog.vue'
|
||||
import ModalDialog from '../shared/ModalDialog.vue'
|
||||
import { eventBus } from '@/common/eventBus'
|
||||
//import '@/assets/view-shared.css'
|
||||
@@ -16,17 +17,22 @@ import type {
|
||||
Task,
|
||||
RewardStatus,
|
||||
ChildTask,
|
||||
ChildRoutine,
|
||||
ChildTaskTriggeredEventPayload,
|
||||
ChildRewardTriggeredEventPayload,
|
||||
ChildRewardRequestEventPayload,
|
||||
ChildTasksSetEventPayload,
|
||||
ChildRewardsSetEventPayload,
|
||||
ChildRoutinesSetEventPayload,
|
||||
TaskModifiedEventPayload,
|
||||
RewardModifiedEventPayload,
|
||||
RoutineModifiedEventPayload,
|
||||
ChildModifiedEventPayload,
|
||||
ChoreScheduleModifiedPayload,
|
||||
ChoreTimeExtendedPayload,
|
||||
RoutineScheduleModifiedPayload,
|
||||
ChildChoreConfirmationPayload,
|
||||
ChildRoutineConfirmationPayload,
|
||||
ChildOverrideSetPayload,
|
||||
ChildOverrideDeletedPayload,
|
||||
} from '@/common/models'
|
||||
@@ -46,6 +52,7 @@ const router = useRouter()
|
||||
|
||||
const child = ref<Child | null>(null)
|
||||
const tasks = ref<string[]>([])
|
||||
const routines = ref<string[]>([])
|
||||
const rewards = ref<string[]>([])
|
||||
const loading = ref(true)
|
||||
const error = ref<string | null>(null)
|
||||
@@ -56,6 +63,9 @@ const dialogReward = ref<RewardStatus | null>(null)
|
||||
const showChoreConfirmDialog = ref(false)
|
||||
const showChoreCancelDialog = ref(false)
|
||||
const dialogChore = ref<ChildTask | null>(null)
|
||||
const showRoutineConfirmDialog = ref(false)
|
||||
const dialogRoutine = ref<ChildRoutine | null>(null)
|
||||
const childRoutineListRef = ref()
|
||||
|
||||
function handleTaskTriggered(event: Event) {
|
||||
const payload = event.payload as ChildTaskTriggeredEventPayload
|
||||
@@ -87,6 +97,13 @@ function handleChildRewardSet(event: Event) {
|
||||
}
|
||||
}
|
||||
|
||||
function handleChildRoutineSet(event: Event) {
|
||||
const payload = event.payload as ChildRoutinesSetEventPayload
|
||||
if (child.value && payload.child_id == child.value.id) {
|
||||
routines.value = payload.routine_ids
|
||||
}
|
||||
}
|
||||
|
||||
function handleRewardRequest(event: Event) {
|
||||
const payload = event.payload as ChildRewardRequestEventPayload
|
||||
const childId = payload.child_id
|
||||
@@ -222,6 +239,14 @@ const triggerTask = async (task: ChildTask) => {
|
||||
// Kindness / Penalty: speech-only in child mode; point changes are handled in parent mode.
|
||||
}
|
||||
|
||||
const handleRoutineClick = (routine: ChildRoutine) => {
|
||||
// Show routine confirmation dialog
|
||||
dialogRoutine.value = routine
|
||||
setTimeout(() => {
|
||||
showRoutineConfirmDialog.value = true
|
||||
}, 150)
|
||||
}
|
||||
|
||||
function isChoreCompletedToday(item: ChildTask): boolean {
|
||||
if (item.pending_status !== 'approved' || !item.approved_at) return false
|
||||
const approvedDate = new Date(item.approved_at)
|
||||
@@ -273,6 +298,30 @@ function closeChoreCancelDialog() {
|
||||
dialogChore.value = null
|
||||
}
|
||||
|
||||
async function doConfirmRoutine() {
|
||||
if (!child.value?.id || !dialogRoutine.value) return
|
||||
try {
|
||||
const resp = await fetch(`/api/child/${child.value.id}/confirm-routine`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ routine_id: dialogRoutine.value.id }),
|
||||
})
|
||||
if (!resp.ok) {
|
||||
console.error('Failed to confirm routine')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to confirm routine:', err)
|
||||
} finally {
|
||||
showRoutineConfirmDialog.value = false
|
||||
dialogRoutine.value = null
|
||||
}
|
||||
}
|
||||
|
||||
function closeRoutineConfirmDialog() {
|
||||
showRoutineConfirmDialog.value = false
|
||||
dialogRoutine.value = null
|
||||
}
|
||||
|
||||
const triggerReward = (reward: RewardStatus) => {
|
||||
// Cancel any pending speech to avoid conflicts
|
||||
if ('speechSynthesis' in window) {
|
||||
@@ -545,6 +594,7 @@ onMounted(async () => {
|
||||
eventBus.on('child_reward_triggered', handleRewardTriggered)
|
||||
eventBus.on('child_tasks_set', handleChildTaskSet)
|
||||
eventBus.on('child_rewards_set', handleChildRewardSet)
|
||||
eventBus.on('child_routines_set', handleChildRoutineSet)
|
||||
eventBus.on('task_modified', handleTaskModified)
|
||||
eventBus.on('reward_modified', handleRewardModified)
|
||||
eventBus.on('child_modified', handleChildModified)
|
||||
@@ -563,6 +613,7 @@ onMounted(async () => {
|
||||
if (data) {
|
||||
child.value = data
|
||||
tasks.value = data.tasks || []
|
||||
routines.value = data.routines || []
|
||||
rewards.value = data.rewards || []
|
||||
}
|
||||
loading.value = false
|
||||
@@ -582,6 +633,7 @@ onUnmounted(() => {
|
||||
eventBus.off('child_reward_triggered', handleRewardTriggered)
|
||||
eventBus.off('child_tasks_set', handleChildTaskSet)
|
||||
eventBus.off('child_rewards_set', handleChildRewardSet)
|
||||
eventBus.off('child_routines_set', handleChildRoutineSet)
|
||||
eventBus.off('task_modified', handleTaskModified)
|
||||
eventBus.off('reward_modified', handleRewardModified)
|
||||
eventBus.off('child_modified', handleChildModified)
|
||||
@@ -731,6 +783,49 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</template>
|
||||
</ScrollingList>
|
||||
<ScrollingList
|
||||
title="Routines"
|
||||
ref="childRoutineListRef"
|
||||
:fetchBaseUrl="`/api/child/${child?.id}/list-routines`"
|
||||
:ids="routines"
|
||||
itemKey="routines"
|
||||
imageField="image_id"
|
||||
:isParentAuthenticated="false"
|
||||
:readyItemId="readyItemId"
|
||||
@item-ready="handleItemReady"
|
||||
@trigger-item="handleRoutineClick"
|
||||
:getItemClass="
|
||||
(item: ChildRoutine) => ({
|
||||
good: true,
|
||||
'routine-pending': item.pending_status === 'pending',
|
||||
'routine-approved': item.pending_status === 'approved',
|
||||
})
|
||||
"
|
||||
>
|
||||
<template #item="{ item }: { item: ChildRoutine }">
|
||||
<span v-if="item.pending_status === 'pending'" class="chore-stamp pending-stamp"
|
||||
>PENDING</span
|
||||
>
|
||||
<span v-else-if="item.pending_status === 'approved'" class="chore-stamp completed-stamp"
|
||||
>APPROVED</span
|
||||
>
|
||||
<div class="item-name">{{ item.name }}</div>
|
||||
<img
|
||||
v-if="item.image_url"
|
||||
:src="item.image_url"
|
||||
alt="Routine Image"
|
||||
class="item-image"
|
||||
/>
|
||||
<div class="item-points good-points">
|
||||
{{
|
||||
item.custom_value !== undefined && item.custom_value !== null
|
||||
? item.custom_value
|
||||
: item.points
|
||||
}}
|
||||
Points
|
||||
</div>
|
||||
</template>
|
||||
</ScrollingList>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -785,6 +880,16 @@ onUnmounted(() => {
|
||||
<button @click="closeChoreCancelDialog" class="btn btn-secondary">No</button>
|
||||
</div>
|
||||
</ModalDialog>
|
||||
|
||||
<!-- Routine confirm dialog -->
|
||||
<RoutineConfirmDialog
|
||||
:show="showRoutineConfirmDialog"
|
||||
:routineName="dialogRoutine?.name ?? ''"
|
||||
:imageUrl="dialogRoutine?.image_url"
|
||||
:items="dialogRoutine?.items"
|
||||
@confirm="doConfirmRoutine"
|
||||
@cancel="closeRoutineConfirmDialog"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
Reference in New Issue
Block a user