Add routine management features for child and parent views
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 3m8s
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,6 +6,8 @@ import PendingRewardDialog from './PendingRewardDialog.vue'
|
||||
import TaskConfirmDialog from './TaskConfirmDialog.vue'
|
||||
import RewardConfirmDialog from './RewardConfirmDialog.vue'
|
||||
import ChoreApproveDialog from './ChoreApproveDialog.vue'
|
||||
import RoutineConfirmDialog from './RoutineConfirmDialog.vue'
|
||||
import RoutineApproveDialog from './RoutineApproveDialog.vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import ChildDetailCard from './ChildDetailCard.vue'
|
||||
import ScrollingList from '../shared/ScrollingList.vue'
|
||||
@@ -17,6 +19,12 @@ import {
|
||||
approveChore,
|
||||
rejectChore,
|
||||
resetChore,
|
||||
extendRoutineTime,
|
||||
setChildRoutineOverride,
|
||||
approveRoutine,
|
||||
rejectRoutine,
|
||||
resetRoutine,
|
||||
triggerRoutineAsParent,
|
||||
} from '@/common/api'
|
||||
import { eventBus } from '@/common/eventBus'
|
||||
import '@/assets/styles.css'
|
||||
@@ -27,6 +35,7 @@ import type {
|
||||
Reward,
|
||||
RewardStatus,
|
||||
ChildTask,
|
||||
ChildRoutine,
|
||||
ChildTaskTriggeredEventPayload,
|
||||
ChildRewardTriggeredEventPayload,
|
||||
ChildRewardRequestEventPayload,
|
||||
@@ -40,6 +49,8 @@ import type {
|
||||
ChoreScheduleModifiedPayload,
|
||||
ChoreTimeExtendedPayload,
|
||||
ChildChoreConfirmationPayload,
|
||||
RoutineScheduleModifiedPayload,
|
||||
RoutineTimeExtendedPayload,
|
||||
} from '@/common/models'
|
||||
import {
|
||||
isScheduledToday,
|
||||
@@ -74,6 +85,12 @@ const lastEditedItem = ref<{ id: string; type: 'task' | 'reward' } | null>(null)
|
||||
const showChoreApproveDialog = ref(false)
|
||||
const approveDialogChore = ref<ChildTask | null>(null)
|
||||
|
||||
// Routine approve/reject
|
||||
const showRoutineApproveDialog = ref(false)
|
||||
const approveDialogRoutine = ref<ChildRoutine | null>(null)
|
||||
const showRoutineConfirmDialog = ref(false)
|
||||
const confirmDialogRoutine = ref<ChildRoutine | null>(null)
|
||||
|
||||
// Override editing
|
||||
const showOverrideModal = ref(false)
|
||||
const overrideEditTarget = ref<{ entity: Task | Reward; type: 'task' | 'reward' } | null>(null)
|
||||
@@ -95,6 +112,14 @@ const kebabBtnRefs = ref<Map<string, HTMLElement>>(new Map())
|
||||
const showScheduleModal = ref(false)
|
||||
const scheduleTarget = ref<ChildTask | null>(null)
|
||||
|
||||
// Routines
|
||||
const childRoutineListRef = ref()
|
||||
const selectedRoutineId = ref<string | null>(null)
|
||||
const activeRoutineMenuFor = ref<string | null>(null)
|
||||
const routineKebabBtnRefs = ref<Map<string, HTMLElement>>(new Map())
|
||||
const showRoutineScheduleModal = ref(false)
|
||||
const routineScheduleTarget = ref<ChildRoutine | null>(null)
|
||||
|
||||
const pendingDialogReward = computed<Reward | RewardStatus | null>(() => {
|
||||
if (pendingEditOverrideTarget.value?.type === 'reward') {
|
||||
return pendingEditOverrideTarget.value.entity as Reward
|
||||
@@ -112,6 +137,7 @@ const lastFetchDate = ref<string>(toLocalISODate(new Date()))
|
||||
function handleItemReady(itemId: string) {
|
||||
readyItemId.value = itemId
|
||||
selectedChoreId.value = null
|
||||
selectedRoutineId.value = null
|
||||
}
|
||||
|
||||
function handleChoreItemReady(itemId: string) {
|
||||
@@ -263,6 +289,8 @@ function handleOverrideSet(event: Event) {
|
||||
scrollAfterRefresh(childPenaltyListRef)
|
||||
} else if (payload.override.entity_type === 'reward') {
|
||||
scrollAfterRefresh(childRewardListRef)
|
||||
} else if (payload.override.entity_type === 'routine') {
|
||||
scrollAfterRefresh(childRoutineListRef)
|
||||
}
|
||||
lastEditedItem.value = null
|
||||
}
|
||||
@@ -305,6 +333,147 @@ function handleChoreConfirmation(event: Event) {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Routine SSE handlers ──────────────────────────────────────────────────────
|
||||
|
||||
function handleRoutineScheduleModified(event: Event) {
|
||||
const payload = event.payload as RoutineScheduleModifiedPayload
|
||||
if (child.value && payload.child_id === child.value.id) {
|
||||
childRoutineListRef.value?.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
function handleRoutineTimeExtended(event: Event) {
|
||||
const payload = event.payload as RoutineTimeExtendedPayload
|
||||
if (child.value && payload.child_id === child.value.id) {
|
||||
childRoutineListRef.value?.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
function handleRoutineModified() {
|
||||
childRoutineListRef.value?.refresh()
|
||||
}
|
||||
|
||||
function handleChildRoutinesSet(event: Event) {
|
||||
const payload = event.payload as { child_id: string }
|
||||
if (child.value && payload.child_id === child.value.id) {
|
||||
childRoutineListRef.value?.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
function handleChildRoutineConfirmation(event: Event) {
|
||||
const payload = event.payload as { child_id: string }
|
||||
if (child.value && payload.child_id === child.value.id) {
|
||||
childRoutineListRef.value?.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
// ── Routine helpers ───────────────────────────────────────────────────────────
|
||||
|
||||
function isRoutineScheduledToday(item: ChildRoutine): boolean {
|
||||
if (!item.schedule) return true
|
||||
return isScheduledToday(item.schedule as any, new Date())
|
||||
}
|
||||
|
||||
function isRoutineExpired(item: ChildRoutine): boolean {
|
||||
if (item.pending_status === 'pending') return false
|
||||
if (!item.schedule) return false
|
||||
const now = new Date()
|
||||
if (!isScheduledToday(item.schedule as any, now)) return false
|
||||
const due = getDueTimeToday(item.schedule as any, now)
|
||||
if (!due) return false
|
||||
if (isExtendedToday(item.extension_date ?? null, now)) return false
|
||||
return isPastTime(due.hour, due.minute, now)
|
||||
}
|
||||
|
||||
function isRoutinePending(item: ChildRoutine): boolean {
|
||||
return item.pending_status === 'pending'
|
||||
}
|
||||
|
||||
function isRoutineApprovedToday(item: ChildRoutine): boolean {
|
||||
if (item.pending_status !== 'approved' || !item.approved_at) return false
|
||||
const approvedDate = new Date(item.approved_at)
|
||||
const today = new Date()
|
||||
return (
|
||||
approvedDate.getFullYear() === today.getFullYear() &&
|
||||
approvedDate.getMonth() === today.getMonth() &&
|
||||
approvedDate.getDate() === today.getDate()
|
||||
)
|
||||
}
|
||||
|
||||
function isRoutineInactive(item: ChildRoutine): boolean {
|
||||
return !isRoutineScheduledToday(item) || isRoutineExpired(item)
|
||||
}
|
||||
|
||||
function routineDueLabel(item: ChildRoutine): string | null {
|
||||
if (!item.schedule) return null
|
||||
const now = new Date()
|
||||
if (!isScheduledToday(item.schedule as any, now)) return null
|
||||
const due = getDueTimeToday(item.schedule as any, now)
|
||||
if (!due) return null
|
||||
if (isExtendedToday(item.extension_date ?? null, now)) return null
|
||||
if (isPastTime(due.hour, due.minute, now)) return null
|
||||
return `Due by ${formatDueTimeLabel(due.hour, due.minute)}`
|
||||
}
|
||||
|
||||
// ── Routine kebab menu ────────────────────────────────────────────────────────
|
||||
|
||||
function handleRoutineItemReady(itemId: string) {
|
||||
readyItemId.value = itemId
|
||||
selectedRoutineId.value = itemId || null
|
||||
}
|
||||
|
||||
function openRoutineMenu(routineId: string, e: MouseEvent) {
|
||||
e.stopPropagation()
|
||||
const btn = routineKebabBtnRefs.value.get(routineId)
|
||||
if (btn) {
|
||||
const rect = btn.getBoundingClientRect()
|
||||
menuPosition.value = { top: rect.bottom, left: rect.right - 140 }
|
||||
}
|
||||
activeRoutineMenuFor.value = routineId
|
||||
}
|
||||
|
||||
function closeRoutineMenu() {
|
||||
activeRoutineMenuFor.value = null
|
||||
}
|
||||
|
||||
function openRoutineScheduleModal(item: ChildRoutine, e: MouseEvent) {
|
||||
e.stopPropagation()
|
||||
closeRoutineMenu()
|
||||
routineScheduleTarget.value = item
|
||||
showRoutineScheduleModal.value = true
|
||||
}
|
||||
|
||||
function onRoutineScheduleSaved() {
|
||||
showRoutineScheduleModal.value = false
|
||||
routineScheduleTarget.value = null
|
||||
}
|
||||
|
||||
function editRoutine(item: ChildRoutine) {
|
||||
closeRoutineMenu()
|
||||
router.push({ name: 'EditRoutine', params: { id: item.id } })
|
||||
}
|
||||
|
||||
function editRoutinePoints(item: ChildRoutine) {
|
||||
closeRoutineMenu()
|
||||
overrideEditTarget.value = { entity: item as any, type: 'routine' as any }
|
||||
const defaultValue = item.custom_value ?? item.points
|
||||
overrideCustomValue.value = defaultValue
|
||||
validateOverrideInput()
|
||||
showOverrideModal.value = true
|
||||
}
|
||||
|
||||
async function doExtendRoutineTime(item: ChildRoutine, e: MouseEvent) {
|
||||
e.stopPropagation()
|
||||
closeRoutineMenu()
|
||||
if (!child.value) return
|
||||
const today = toLocalISODate(new Date())
|
||||
const res = await extendRoutineTime(child.value.id, item.id, today)
|
||||
if (!res.ok) {
|
||||
const { msg } = await parseErrorResponse(res)
|
||||
alert(`Error: ${msg}`)
|
||||
}
|
||||
}
|
||||
|
||||
function isChoreCompletedToday(item: ChildTask): boolean {
|
||||
if (item.pending_status !== 'approved' || !item.approved_at) return false
|
||||
const approvedDate = new Date(item.approved_at)
|
||||
@@ -363,6 +532,111 @@ function cancelChoreApproveDialog() {
|
||||
approveDialogChore.value = null
|
||||
}
|
||||
|
||||
// ── Routine approve/reject ────────────────────────────────────────────────────
|
||||
|
||||
async function doApproveRoutine() {
|
||||
if (!child.value || !approveDialogRoutine.value) return
|
||||
try {
|
||||
const confirmationId = approveDialogRoutine.value.pending_confirmation_id
|
||||
if (!confirmationId) return
|
||||
const resp = await approveRoutine(child.value.id, confirmationId)
|
||||
if (resp.ok) {
|
||||
const data = await resp.json()
|
||||
if (child.value) child.value.points = data.points
|
||||
} else {
|
||||
const { msg } = await parseErrorResponse(resp)
|
||||
alert(`Error: ${msg}`)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to approve routine:', err)
|
||||
} finally {
|
||||
showRoutineApproveDialog.value = false
|
||||
approveDialogRoutine.value = null
|
||||
}
|
||||
}
|
||||
|
||||
async function doRejectRoutine() {
|
||||
if (!child.value || !approveDialogRoutine.value) return
|
||||
const confirmationId = approveDialogRoutine.value.pending_confirmation_id
|
||||
if (!confirmationId) return
|
||||
try {
|
||||
const resp = await rejectRoutine(child.value.id, confirmationId)
|
||||
if (!resp.ok) {
|
||||
const { msg } = await parseErrorResponse(resp)
|
||||
alert(`Error: ${msg}`)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to reject routine:', err)
|
||||
} finally {
|
||||
showRoutineApproveDialog.value = false
|
||||
approveDialogRoutine.value = null
|
||||
}
|
||||
}
|
||||
|
||||
function cancelRoutineApproveDialog() {
|
||||
showRoutineApproveDialog.value = false
|
||||
approveDialogRoutine.value = null
|
||||
}
|
||||
|
||||
async function doConfirmRoutine() {
|
||||
if (!child.value || !confirmDialogRoutine.value) return
|
||||
try {
|
||||
const resp = await triggerRoutineAsParent(child.value.id, confirmDialogRoutine.value.id)
|
||||
if (resp.ok) {
|
||||
const data = await resp.json()
|
||||
if (child.value) child.value.points = data.points
|
||||
} else {
|
||||
const { msg } = await parseErrorResponse(resp)
|
||||
alert(`Error: ${msg}`)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to confirm routine:', err)
|
||||
} finally {
|
||||
showRoutineConfirmDialog.value = false
|
||||
confirmDialogRoutine.value = null
|
||||
}
|
||||
}
|
||||
|
||||
function cancelRoutineConfirmDialog() {
|
||||
showRoutineConfirmDialog.value = false
|
||||
confirmDialogRoutine.value = null
|
||||
}
|
||||
|
||||
async function doResetRoutine(item: ChildRoutine, e: MouseEvent) {
|
||||
e.stopPropagation()
|
||||
closeRoutineMenu()
|
||||
if (!child.value || !item.pending_confirmation_id) return
|
||||
try {
|
||||
const resp = await resetRoutine(child.value.id, item.pending_confirmation_id)
|
||||
if (!resp.ok) {
|
||||
const { msg } = await parseErrorResponse(resp)
|
||||
alert(`Error: ${msg}`)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to reset routine:', err)
|
||||
}
|
||||
}
|
||||
|
||||
function triggerRoutine(item: ChildRoutine) {
|
||||
if (shouldIgnoreNextCardClick.value) {
|
||||
shouldIgnoreNextCardClick.value = false
|
||||
return
|
||||
}
|
||||
if (isRoutineApprovedToday(item)) return
|
||||
if (isRoutineExpired(item)) return
|
||||
if (isRoutinePending(item)) {
|
||||
approveDialogRoutine.value = item
|
||||
setTimeout(() => {
|
||||
showRoutineApproveDialog.value = true
|
||||
}, 150)
|
||||
return
|
||||
}
|
||||
confirmDialogRoutine.value = item
|
||||
setTimeout(() => {
|
||||
showRoutineConfirmDialog.value = true
|
||||
}, 150)
|
||||
}
|
||||
|
||||
async function doResetChore(item: ChildTask, e: MouseEvent) {
|
||||
e.stopPropagation()
|
||||
closeChoreMenu()
|
||||
@@ -381,7 +655,7 @@ async function doResetChore(item: ChildTask, e: MouseEvent) {
|
||||
// ── Kebab menu ───────────────────────────────────────────────────────────────
|
||||
|
||||
const onDocClick = (e: MouseEvent) => {
|
||||
if (activeMenuFor.value !== null) {
|
||||
if (activeMenuFor.value !== null || activeRoutineMenuFor.value !== null) {
|
||||
const path = (e.composedPath?.() ?? (e as any).path ?? []) as EventTarget[]
|
||||
const inside = path.some((node) => {
|
||||
if (!(node instanceof HTMLElement)) return false
|
||||
@@ -393,11 +667,12 @@ const onDocClick = (e: MouseEvent) => {
|
||||
})
|
||||
if (!inside) {
|
||||
activeMenuFor.value = null
|
||||
activeRoutineMenuFor.value = null
|
||||
selectedChoreId.value = null
|
||||
selectedRoutineId.value = null
|
||||
if (path.some((n) => n instanceof HTMLElement && n.classList.contains('item-card'))) {
|
||||
shouldIgnoreNextCardClick.value = true
|
||||
} else {
|
||||
// Clicked fully outside any card — reset ready state so chore requires 2 clicks again
|
||||
readyItemId.value = null
|
||||
}
|
||||
}
|
||||
@@ -549,7 +824,7 @@ function handleEditItem(item: Task | Reward, type: 'task' | 'reward') {
|
||||
}
|
||||
overrideEditTarget.value = { entity: item, type }
|
||||
const defaultValue = type === 'task' ? (item as Task).points : (item as Reward).cost
|
||||
overrideCustomValue.value = item.custom_value ?? defaultValue
|
||||
overrideCustomValue.value = (item as any).custom_value ?? defaultValue
|
||||
validateOverrideInput()
|
||||
showOverrideModal.value = true
|
||||
}
|
||||
@@ -570,7 +845,7 @@ async function confirmPendingRewardAndEdit() {
|
||||
overrideEditTarget.value = target
|
||||
const defaultValue =
|
||||
target.type === 'task' ? (target.entity as Task).points : (target.entity as Reward).cost
|
||||
overrideCustomValue.value = target.entity.custom_value ?? defaultValue
|
||||
overrideCustomValue.value = (target.entity as any).custom_value ?? defaultValue
|
||||
validateOverrideInput()
|
||||
showOverrideModal.value = true
|
||||
}
|
||||
@@ -590,12 +865,22 @@ watch(showOverrideModal, async (newVal) => {
|
||||
async function saveOverride() {
|
||||
if (!isOverrideValid.value || !overrideEditTarget.value || !child.value) return
|
||||
|
||||
const res = await setChildOverride(
|
||||
child.value.id,
|
||||
overrideEditTarget.value.entity.id,
|
||||
overrideEditTarget.value.type,
|
||||
overrideCustomValue.value,
|
||||
)
|
||||
const type = overrideEditTarget.value.type as string
|
||||
let res: Response
|
||||
if (type === 'routine') {
|
||||
res = await setChildRoutineOverride(
|
||||
child.value.id,
|
||||
overrideEditTarget.value.entity.id,
|
||||
overrideCustomValue.value,
|
||||
)
|
||||
} else {
|
||||
res = await setChildOverride(
|
||||
child.value.id,
|
||||
overrideEditTarget.value.entity.id,
|
||||
overrideEditTarget.value.type,
|
||||
overrideCustomValue.value,
|
||||
)
|
||||
}
|
||||
|
||||
if (res.ok) {
|
||||
lastEditedItem.value = {
|
||||
@@ -604,7 +889,7 @@ async function saveOverride() {
|
||||
}
|
||||
showOverrideModal.value = false
|
||||
} else {
|
||||
const { msg } = parseErrorResponse(res)
|
||||
const { msg } = await parseErrorResponse(res)
|
||||
alert(`Error: ${msg}`)
|
||||
}
|
||||
}
|
||||
@@ -683,6 +968,11 @@ onMounted(async () => {
|
||||
eventBus.on('chore_schedule_modified', handleChoreScheduleModified)
|
||||
eventBus.on('chore_time_extended', handleChoreTimeExtended)
|
||||
eventBus.on('child_chore_confirmation', handleChoreConfirmation)
|
||||
eventBus.on('routine_schedule_modified', handleRoutineScheduleModified)
|
||||
eventBus.on('routine_time_extended', handleRoutineTimeExtended)
|
||||
eventBus.on('routine_modified', handleRoutineModified)
|
||||
eventBus.on('child_routines_set', handleChildRoutinesSet)
|
||||
eventBus.on('child_routine_confirmation', handleChildRoutineConfirmation)
|
||||
|
||||
document.addEventListener('click', onDocClick, true)
|
||||
document.addEventListener('visibilitychange', onVisibilityChange)
|
||||
@@ -722,6 +1012,9 @@ onMounted(async () => {
|
||||
} else if (entityType === 'reward') {
|
||||
childRewardListRef.value?.scrollToItem(scrollToId)
|
||||
applyHighlightPulse(scrollToId)
|
||||
} else if (entityType === 'routine') {
|
||||
childRoutineListRef.value?.scrollToItem(scrollToId)
|
||||
applyHighlightPulse(scrollToId)
|
||||
}
|
||||
const { scrollTo: _s, entityType: _e, ...remainingQuery } = route.query
|
||||
router.replace({ query: remainingQuery })
|
||||
@@ -749,6 +1042,11 @@ onUnmounted(() => {
|
||||
eventBus.off('chore_schedule_modified', handleChoreScheduleModified)
|
||||
eventBus.off('chore_time_extended', handleChoreTimeExtended)
|
||||
eventBus.off('child_chore_confirmation', handleChoreConfirmation)
|
||||
eventBus.off('routine_schedule_modified', handleRoutineScheduleModified)
|
||||
eventBus.off('routine_time_extended', handleRoutineTimeExtended)
|
||||
eventBus.off('routine_modified', handleRoutineModified)
|
||||
eventBus.off('child_routines_set', handleChildRoutinesSet)
|
||||
eventBus.off('child_routine_confirmation', handleChildRoutineConfirmation)
|
||||
|
||||
document.removeEventListener('click', onDocClick, true)
|
||||
document.removeEventListener('visibilitychange', onVisibilityChange)
|
||||
@@ -1058,6 +1356,127 @@ function goToAssignRoutines() {
|
||||
<div v-if="choreDueLabel(item)" class="due-label">{{ choreDueLabel(item) }}</div>
|
||||
</template>
|
||||
</ScrollingList>
|
||||
<ScrollingList
|
||||
title="Routines"
|
||||
ref="childRoutineListRef"
|
||||
:fetchBaseUrl="`/api/child/${child?.id}/list-routines`"
|
||||
itemKey="routines"
|
||||
imageField="image_id"
|
||||
:enableEdit="false"
|
||||
:childId="child?.id"
|
||||
:readyItemId="readyItemId"
|
||||
:isParentAuthenticated="true"
|
||||
@item-ready="handleRoutineItemReady"
|
||||
@trigger-item="triggerRoutine"
|
||||
:getItemClass="
|
||||
(item) => ({
|
||||
good: true,
|
||||
'chore-inactive': isRoutineInactive(item) || isRoutineApprovedToday(item),
|
||||
})
|
||||
"
|
||||
:sort-fn="
|
||||
(a: ChildRoutine, b: ChildRoutine) => {
|
||||
if (isRoutinePending(a) !== isRoutinePending(b)) return isRoutinePending(a) ? -1 : 1
|
||||
if (!isRoutineScheduledToday(a) !== !isRoutineScheduledToday(b))
|
||||
return isRoutineScheduledToday(a) ? -1 : 1
|
||||
if (isRoutineApprovedToday(a) !== isRoutineApprovedToday(b))
|
||||
return isRoutineApprovedToday(a) ? 1 : -1
|
||||
if (isRoutineExpired(a) !== isRoutineExpired(b)) return isRoutineExpired(a) ? 1 : -1
|
||||
return 0
|
||||
}
|
||||
"
|
||||
>
|
||||
<template #item="{ item }: { item: ChildRoutine }">
|
||||
<!-- Routine kebab menu -->
|
||||
<div class="chore-kebab-wrap" @click.stop>
|
||||
<button
|
||||
v-show="selectedRoutineId === item.id"
|
||||
class="kebab-btn"
|
||||
:ref="
|
||||
(el) => {
|
||||
if (el) routineKebabBtnRefs.set(item.id, el as HTMLElement)
|
||||
else routineKebabBtnRefs.delete(item.id)
|
||||
}
|
||||
"
|
||||
@mousedown.stop.prevent
|
||||
@click="openRoutineMenu(item.id, $event)"
|
||||
:aria-expanded="activeRoutineMenuFor === item.id ? 'true' : 'false'"
|
||||
aria-label="Options"
|
||||
>
|
||||
⋮
|
||||
</button>
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="activeRoutineMenuFor === item.id"
|
||||
class="kebab-menu"
|
||||
:style="{ top: menuPosition.top + 'px', left: menuPosition.left + 'px' }"
|
||||
@mousedown.stop.prevent
|
||||
@click.stop
|
||||
>
|
||||
<button class="menu-item" @mousedown.stop.prevent @click="editRoutine(item)">
|
||||
Edit Routine
|
||||
</button>
|
||||
<button
|
||||
class="menu-item"
|
||||
@mousedown.stop.prevent
|
||||
@click="editRoutinePoints(item)"
|
||||
>
|
||||
Edit Points
|
||||
</button>
|
||||
<button
|
||||
class="menu-item"
|
||||
@mousedown.stop.prevent
|
||||
@click="openRoutineScheduleModal(item, $event)"
|
||||
>
|
||||
Schedule
|
||||
</button>
|
||||
<button
|
||||
v-if="isRoutineExpired(item)"
|
||||
class="menu-item"
|
||||
@mousedown.stop.prevent
|
||||
@click="doExtendRoutineTime(item, $event)"
|
||||
>
|
||||
Extend Time
|
||||
</button>
|
||||
<button
|
||||
v-if="isRoutineApprovedToday(item)"
|
||||
class="menu-item"
|
||||
@mousedown.stop.prevent
|
||||
@click="doResetRoutine(item, $event)"
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</Teleport>
|
||||
</div>
|
||||
|
||||
<!-- Status badges -->
|
||||
<span v-if="isRoutineApprovedToday(item)" class="chore-stamp completed-stamp"
|
||||
>COMPLETED</span
|
||||
>
|
||||
<span v-else-if="isRoutineExpired(item)" class="chore-stamp">TOO LATE</span>
|
||||
<span v-else-if="isRoutinePending(item)" class="chore-stamp pending-stamp"
|
||||
>PENDING</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>
|
||||
<div v-if="routineDueLabel(item)" class="due-label">{{ routineDueLabel(item) }}</div>
|
||||
</template>
|
||||
</ScrollingList>
|
||||
<ScrollingList
|
||||
title="Kindness Acts"
|
||||
ref="childKindnessListRef"
|
||||
@@ -1217,6 +1636,17 @@ function goToAssignRoutines() {
|
||||
@cancelled="showScheduleModal = false"
|
||||
/>
|
||||
|
||||
<!-- Schedule Modal (Routines) -->
|
||||
<ScheduleModal
|
||||
v-if="showRoutineScheduleModal && routineScheduleTarget && child"
|
||||
:entity="routineScheduleTarget"
|
||||
entityType="routine"
|
||||
:childId="child.id"
|
||||
:schedule="routineScheduleTarget.schedule ?? null"
|
||||
@saved="onRoutineScheduleSaved"
|
||||
@cancelled="showRoutineScheduleModal = false"
|
||||
/>
|
||||
|
||||
<!-- Override Edit Modal -->
|
||||
<ModalDialog
|
||||
v-if="showOverrideModal && overrideEditTarget && child"
|
||||
@@ -1263,7 +1693,7 @@ function goToAssignRoutines() {
|
||||
<!-- Reward Confirm Dialog -->
|
||||
<RewardConfirmDialog
|
||||
v-if="showRewardConfirm"
|
||||
:reward="selectedReward"
|
||||
:reward="selectedReward as any"
|
||||
:childName="child?.name"
|
||||
@confirm="confirmTriggerReward"
|
||||
@deny="denyRewardRequest"
|
||||
@@ -1287,6 +1717,26 @@ function goToAssignRoutines() {
|
||||
@reject="doRejectChore"
|
||||
@cancel="cancelChoreApproveDialog"
|
||||
/>
|
||||
|
||||
<!-- Routine Approve/Reject Dialog -->
|
||||
<RoutineApproveDialog
|
||||
v-if="showRoutineApproveDialog && approveDialogRoutine"
|
||||
:show="showRoutineApproveDialog"
|
||||
:childName="child?.name ?? ''"
|
||||
:routineName="approveDialogRoutine.name"
|
||||
:points="approveDialogRoutine.custom_value ?? approveDialogRoutine.points"
|
||||
:imageUrl="approveDialogRoutine.image_url"
|
||||
@approve="doApproveRoutine"
|
||||
@reject="doRejectRoutine"
|
||||
@cancel="cancelRoutineApproveDialog"
|
||||
/>
|
||||
|
||||
<RoutineConfirmDialog
|
||||
:routine="confirmDialogRoutine"
|
||||
:childName="child?.name ?? ''"
|
||||
@confirm="doConfirmRoutine"
|
||||
@cancel="cancelRoutineConfirmDialog"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user