Add routine management features for child and parent views
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 3m8s

- Implemented routine child-mode flow tests to ensure proper functionality of routine assignment and task completion.
- Created notification tests for parent view to verify routine completion notifications for children.
- Developed ChildRoutineOverlay component for displaying routine tasks and handling user interactions.
- Added RoutineApproveDialog component for approving or rejecting completed routines.
- Created unit tests for ChildRoutineOverlay and RoutineEditView components to ensure correct behavior and rendering.
- Enhanced RoutineEditView with proper handling of task addition and form submission.

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
2026-05-17 23:47:12 -04:00
parent eb775ba7d8
commit 5392e5af70
31 changed files with 3859 additions and 265 deletions

View File

@@ -6,7 +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 ChildRoutineOverlay from './ChildRoutineOverlay.vue'
import ModalDialog from '../shared/ModalDialog.vue'
import { eventBus } from '@/common/eventBus'
//import '@/assets/view-shared.css'
@@ -14,10 +14,11 @@ import '@/assets/styles.css'
import type {
Child,
Event,
Task,
ChoreSchedule,
RewardStatus,
ChildTask,
ChildRoutine,
RoutineItem,
ChildTaskTriggeredEventPayload,
ChildRewardTriggeredEventPayload,
ChildRewardRequestEventPayload,
@@ -26,17 +27,19 @@ import type {
ChildRoutinesSetEventPayload,
TaskModifiedEventPayload,
RewardModifiedEventPayload,
RoutineModifiedEventPayload,
ChildModifiedEventPayload,
ChoreScheduleModifiedPayload,
ChoreTimeExtendedPayload,
RoutineScheduleModifiedPayload,
ChildChoreConfirmationPayload,
ChildRoutineConfirmationPayload,
ChildOverrideSetPayload,
ChildOverrideDeletedPayload,
} from '@/common/models'
import { confirmChore, cancelConfirmChore } from '@/common/api'
import {
confirmChore,
cancelConfirmChore,
confirmRoutine,
cancelRoutineConfirmation,
} from '@/common/api'
import {
isScheduledToday,
isPastTime,
@@ -63,7 +66,7 @@ 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 showRoutineOverlay = ref(false)
const dialogRoutine = ref<ChildRoutine | null>(null)
const childRoutineListRef = ref()
@@ -240,13 +243,27 @@ const triggerTask = async (task: ChildTask) => {
}
const handleRoutineClick = (routine: ChildRoutine) => {
// Show routine confirmation dialog
dialogRoutine.value = routine
setTimeout(() => {
showRoutineConfirmDialog.value = true
showRoutineOverlay.value = true
}, 150)
}
function speakText(text: string) {
if (!('speechSynthesis' in window)) return
window.speechSynthesis.cancel()
if (!text) return
const utter = new window.SpeechSynthesisUtterance(text)
utter.rate = 1.0
utter.pitch = 1.0
utter.volume = 1.0
window.speechSynthesis.speak(utter)
}
function handleRoutineTaskClick(item: RoutineItem) {
speakText(item.name)
}
function isChoreCompletedToday(item: ChildTask): boolean {
if (item.pending_status !== 'approved' || !item.approved_at) return false
const approvedDate = new Date(item.approved_at)
@@ -301,27 +318,45 @@ function closeChoreCancelDialog() {
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 }),
})
const resp = await confirmRoutine(child.value.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
showRoutineOverlay.value = false
dialogRoutine.value = null
}
}
function closeRoutineConfirmDialog() {
showRoutineConfirmDialog.value = false
function handleChildRoutineConfirmation(event: Event) {
const payload = event.payload as { child_id: string }
if (child.value && payload.child_id === child.value.id) {
childRoutineListRef.value?.refresh()
}
}
function closeRoutineOverlay() {
showRoutineOverlay.value = false
dialogRoutine.value = null
}
async function doCancelConfirmRoutine() {
if (!child.value?.id || !dialogRoutine.value) return
try {
const resp = await cancelRoutineConfirmation(child.value.id, dialogRoutine.value.id)
if (!resp.ok) {
console.error('Failed to cancel routine confirmation')
}
} catch (err) {
console.error('Failed to cancel routine confirmation:', err)
} finally {
showRoutineOverlay.value = false
dialogRoutine.value = null
}
}
const triggerReward = (reward: RewardStatus) => {
// Cancel any pending speech to avoid conflicts
if ('speechSynthesis' in window) {
@@ -510,6 +545,22 @@ function isChoreExpired(item: ChildTask): boolean {
return isPastTime(due.hour, due.minute, today)
}
function isRoutineExpired(item: ChildRoutine): boolean {
if (item.pending_status === 'pending') return false
if (!item.schedule) return false
const today = new Date()
const due = getDueTimeToday(item.schedule as unknown as ChoreSchedule, today)
if (!due) return false
if (item.extension_date && isExtendedToday(item.extension_date, today)) return false
return isPastTime(due.hour, due.minute, today)
}
const canCompleteRoutine = computed(() => {
const routine = dialogRoutine.value
if (!routine) return false
return routine.pending_status == null && !isRoutineExpired(routine)
})
function choreDueLabel(item: ChildTask): string | null {
if (!item.schedule) return null
const today = new Date()
@@ -602,6 +653,7 @@ onMounted(async () => {
eventBus.on('chore_schedule_modified', handleChoreScheduleModified)
eventBus.on('chore_time_extended', handleChoreTimeExtended)
eventBus.on('child_chore_confirmation', handleChoreConfirmation)
eventBus.on('child_routine_confirmation', handleChildRoutineConfirmation)
eventBus.on('child_override_set', handleOverrideSet)
eventBus.on('child_override_deleted', handleOverrideDeleted)
document.addEventListener('visibilitychange', onVisibilityChange)
@@ -641,6 +693,7 @@ onUnmounted(() => {
eventBus.off('chore_schedule_modified', handleChoreScheduleModified)
eventBus.off('chore_time_extended', handleChoreTimeExtended)
eventBus.off('child_chore_confirmation', handleChoreConfirmation)
eventBus.off('child_routine_confirmation', handleChildRoutineConfirmation)
eventBus.off('child_override_set', handleOverrideSet)
eventBus.off('child_override_deleted', handleOverrideDeleted)
document.removeEventListener('visibilitychange', onVisibilityChange)
@@ -882,13 +935,15 @@ onUnmounted(() => {
</ModalDialog>
<!-- Routine confirm dialog -->
<RoutineConfirmDialog
:show="showRoutineConfirmDialog"
:routineName="dialogRoutine?.name ?? ''"
:imageUrl="dialogRoutine?.image_url"
:items="dialogRoutine?.items"
@confirm="doConfirmRoutine"
@cancel="closeRoutineConfirmDialog"
<ChildRoutineOverlay
:show="showRoutineOverlay"
:routine="dialogRoutine"
:canComplete="canCompleteRoutine"
:isPending="dialogRoutine?.pending_status === 'pending'"
@task-click="handleRoutineTaskClick"
@complete="doConfirmRoutine"
@cancel-pending="doCancelConfirmRoutine"
@close="closeRoutineOverlay"
/>
</template>