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
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:
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user