added fixes for bug plan 1.0.6RC01
Some checks failed
Chore App Build, Test, and Push Docker Images / build-and-push (push) Failing after 5m43s

This commit is contained in:
2026-03-24 15:31:57 -04:00
parent 81169da05e
commit e9f4343426
27 changed files with 890 additions and 65 deletions

View File

@@ -21,8 +21,9 @@
<input
v-model="code"
maxlength="6"
class="code-input"
class="code-input uppercase"
placeholder="6-digit code"
@input="code = code.toUpperCase()"
@keyup.enter="isCodeValid && verifyCode()"
/>
<div class="button-group">
@@ -260,6 +261,9 @@ function goBack() {
box-sizing: border-box;
text-align: center;
}
.uppercase {
text-transform: uppercase;
}
.button-group {
display: flex;
gap: 1rem;

View File

@@ -27,6 +27,8 @@ import type {
ChoreScheduleModifiedPayload,
ChoreTimeExtendedPayload,
ChildChoreConfirmationPayload,
ChildOverrideSetPayload,
ChildOverrideDeletedPayload,
} from '@/common/models'
import { confirmChore, cancelConfirmChore } from '@/common/api'
import {
@@ -380,6 +382,8 @@ function removeInactivityListeners() {
}
const childChoreListRef = ref()
const childKindnessListRef = ref()
const childPenaltyListRef = ref()
const readyItemId = ref<string | null>(null)
const expiryTimers = ref<number[]>([])
const lastFetchDate = ref<string>(toLocalISODate(new Date()))
@@ -411,6 +415,32 @@ function handleChoreConfirmation(event: Event) {
}
}
function handleOverrideSet(event: Event) {
const payload = event.payload as ChildOverrideSetPayload
if (child.value && payload.override.child_id === child.value.id) {
if (payload.override.entity_type === 'task') {
childChoreListRef.value?.refresh()
childKindnessListRef.value?.refresh()
childPenaltyListRef.value?.refresh()
} else if (payload.override.entity_type === 'reward') {
childRewardListRef.value?.refresh()
}
}
}
function handleOverrideDeleted(event: Event) {
const payload = event.payload as ChildOverrideDeletedPayload
if (child.value && payload.child_id === child.value.id) {
if (payload.entity_type === 'task') {
childChoreListRef.value?.refresh()
childKindnessListRef.value?.refresh()
childPenaltyListRef.value?.refresh()
} else if (payload.entity_type === 'reward') {
childRewardListRef.value?.refresh()
}
}
}
function isChoreScheduledToday(item: ChildTask): boolean {
if (!item.schedule) return true
const today = new Date()
@@ -435,6 +465,35 @@ function choreDueLabel(item: ChildTask): string | null {
return `Due by ${formatDueTimeLabel(due.hour, due.minute)}`
}
// ── Sorting ───────────────────────────────────────────────────────────────────
function childChoreSortPriority(item: ChildTask): number {
if (item.pending_status === 'pending') return 2
if (!item.schedule) return 1 // general chore
return 0 // scheduled for today
}
function childChoreSort(a: ChildTask, b: ChildTask): number {
const pa = childChoreSortPriority(a)
const pb = childChoreSortPriority(b)
if (pa !== pb) return pa - pb
// Both scheduled: sort by earliest deadline
if (pa === 0) {
const now = new Date()
const dueA = getDueTimeToday(a.schedule!, now)
const dueB = getDueTimeToday(b.schedule!, now)
const minsA = dueA ? dueA.hour * 60 + dueA.minute : Infinity
const minsB = dueB ? dueB.hour * 60 + dueB.minute : Infinity
return minsA - minsB
}
return 0
}
function childRewardSort(a: RewardStatus, b: RewardStatus): number {
if (a.redeeming !== b.redeeming) return a.redeeming ? -1 : 1
return a.points_needed - b.points_needed
}
function clearExpiryTimers() {
expiryTimers.value.forEach((t) => clearTimeout(t))
expiryTimers.value = []
@@ -487,6 +546,8 @@ onMounted(async () => {
eventBus.on('chore_schedule_modified', handleChoreScheduleModified)
eventBus.on('chore_time_extended', handleChoreTimeExtended)
eventBus.on('child_chore_confirmation', handleChoreConfirmation)
eventBus.on('child_override_set', handleOverrideSet)
eventBus.on('child_override_deleted', handleOverrideDeleted)
document.addEventListener('visibilitychange', onVisibilityChange)
if (route.params.id) {
const idParam = Array.isArray(route.params.id) ? route.params.id[0] : route.params.id
@@ -522,6 +583,8 @@ onUnmounted(() => {
eventBus.off('chore_schedule_modified', handleChoreScheduleModified)
eventBus.off('chore_time_extended', handleChoreTimeExtended)
eventBus.off('child_chore_confirmation', handleChoreConfirmation)
eventBus.off('child_override_set', handleOverrideSet)
eventBus.off('child_override_deleted', handleOverrideDeleted)
document.removeEventListener('visibilitychange', onVisibilityChange)
clearExpiryTimers()
removeInactivityListeners()
@@ -555,6 +618,7 @@ onUnmounted(() => {
})
"
:filter-fn="(item: ChildTask) => item.type === 'chore' && isChoreScheduledToday(item)"
:sort-fn="childChoreSort"
>
<template #item="{ item }: { item: ChildTask }">
<span v-if="isChoreExpired(item)" class="chore-stamp">TOO LATE</span>
@@ -590,6 +654,7 @@ onUnmounted(() => {
:getItemClass="
(item) => ({ reward: true, disabled: hasPendingRewards && !item.redeeming })
"
:sort-fn="childRewardSort"
>
<template #item="{ item }: { item: RewardStatus }">
<div class="item-name">{{ item.name }}</div>

View File

@@ -1,6 +1,6 @@
<template>
<div class="assign-view">
<h2>Assign Chores</h2>
<h2>Assign Chores{{ childName ? ` for ${childName}` : '' }}</h2>
<div class="list-container">
<MessageBlock v-if="countRef === 0" message="No chores">
<span> <button class="round-btn" @click="goToCreate">Create</button> a chore </span>
@@ -41,6 +41,7 @@ import { TASK_FIELDS } from '@/common/models'
const route = useRoute()
const router = useRouter()
const childId = route.params.id
const childName = typeof route.query.name === 'string' ? route.query.name : ''
const listRef = ref()
const countRef = ref(-1)

View File

@@ -1,6 +1,6 @@
<template>
<div class="assign-view">
<h2>Assign Kindness Acts</h2>
<h2>Assign Kindness Acts{{ childName ? ` for ${childName}` : '' }}</h2>
<div class="list-container">
<MessageBlock v-if="countRef === 0" message="No kindness acts">
<span> <button class="round-btn" @click="goToCreate">Create</button> a kindness act </span>
@@ -41,6 +41,7 @@ import { TASK_FIELDS } from '@/common/models'
const route = useRoute()
const router = useRouter()
const childId = route.params.id
const childName = typeof route.query.name === 'string' ? route.query.name : ''
const listRef = ref()
const countRef = ref(-1)

View File

@@ -68,6 +68,7 @@ const childPenaltyListRef = ref()
const childRewardListRef = ref()
const childKindnessListRef = ref()
const showPendingRewardDialog = ref(false)
const lastEditedItem = ref<{ id: string; type: 'task' | 'reward' } | null>(null)
// Chore approve/reject
const showChoreApproveDialog = ref(false)
@@ -243,14 +244,20 @@ function handleChildModified(event: Event) {
function handleOverrideSet(event: Event) {
const payload = event.payload as ChildOverrideSetPayload
if (child.value && payload.override.child_id === child.value.id) {
// Refresh the appropriate list to show the override badge
if (payload.override.entity_type === 'task') {
childChoreListRef.value?.refresh()
childKindnessListRef.value?.refresh()
childPenaltyListRef.value?.refresh()
} else if (payload.override.entity_type === 'reward') {
childRewardListRef.value?.refresh()
const editedId = lastEditedItem.value?.id ?? null
const scrollAfterRefresh = (listRef: typeof childChoreListRef) => {
listRef.value?.refresh().then(() => {
if (editedId) listRef.value?.scrollToItem(editedId)
})
}
if (payload.override.entity_type === 'task') {
scrollAfterRefresh(childChoreListRef)
scrollAfterRefresh(childKindnessListRef)
scrollAfterRefresh(childPenaltyListRef)
} else if (payload.override.entity_type === 'reward') {
scrollAfterRefresh(childRewardListRef)
}
lastEditedItem.value = null
}
}
@@ -424,6 +431,7 @@ async function doExtendTime(item: ChildTask, e: MouseEvent) {
alert(`Error: ${msg}`)
return
}
childChoreListRef.value?.refresh()
setTimeout(() => resetExpiryTimers(), 300)
}
@@ -459,6 +467,25 @@ function isChoreInactive(item: ChildTask): boolean {
return !isChoreScheduledToday(item) || isChoreExpired(item)
}
// ── Sorting ───────────────────────────────────────────────────────────────────
function parentChoreSortPriority(item: ChildTask): number {
if (isChorePending(item)) return 0
if (!isChoreScheduledToday(item)) return 4
if (isChoreCompletedToday(item)) return 3
if (isChoreExpired(item)) return 2
return 1 // general / active
}
function parentChoreSort(a: ChildTask, b: ChildTask): number {
return parentChoreSortPriority(a) - parentChoreSortPriority(b)
}
function parentRewardSort(a: RewardStatus, b: RewardStatus): number {
if (a.redeeming !== b.redeeming) return a.redeeming ? -1 : 1
return 0
}
// ── Expiry timers ─────────────────────────────────────────────────────────────
function clearExpiryTimers() {
@@ -556,6 +583,10 @@ async function saveOverride() {
)
if (res.ok) {
lastEditedItem.value = {
id: overrideEditTarget.value.entity.id,
type: overrideEditTarget.value.type,
}
showOverrideModal.value = false
} else {
const { msg } = parseErrorResponse(res)
@@ -600,6 +631,8 @@ onMounted(async () => {
if (route.params.id) {
const idParam = Array.isArray(route.params.id) ? route.params.id[0] : route.params.id
const scrollToId = typeof route.query.scrollTo === 'string' ? route.query.scrollTo : null
const entityType = typeof route.query.entityType === 'string' ? route.query.entityType : null
if (idParam !== undefined) {
const promise = fetchChildData(idParam)
promise.then((data) => {
@@ -609,6 +642,15 @@ onMounted(async () => {
rewards.value = data.rewards || []
}
loading.value = false
if (scrollToId) {
setTimeout(() => {
if (entityType === 'chore') {
childChoreListRef.value?.scrollToItem(scrollToId)
} else if (entityType === 'reward') {
childRewardListRef.value?.scrollToItem(scrollToId)
}
}, 500)
}
})
}
}
@@ -765,25 +807,41 @@ const confirmTriggerReward = async () => {
function goToAssignTasks() {
if (child.value?.id) {
router.push({ name: 'ChoreAssignView', params: { id: child.value.id } })
router.push({
name: 'ChoreAssignView',
params: { id: child.value.id },
query: { name: child.value.name },
})
}
}
function goToAssignBadHabits() {
if (child.value?.id) {
router.push({ name: 'PenaltyAssignView', params: { id: child.value.id } })
router.push({
name: 'PenaltyAssignView',
params: { id: child.value.id },
query: { name: child.value.name },
})
}
}
function goToAssignKindness() {
if (child.value?.id) {
router.push({ name: 'KindnessAssignView', params: { id: child.value.id } })
router.push({
name: 'KindnessAssignView',
params: { id: child.value.id },
query: { name: child.value.name },
})
}
}
function goToAssignRewards() {
if (child.value?.id) {
router.push({ name: 'RewardAssignView', params: { id: child.value.id } })
router.push({
name: 'RewardAssignView',
params: { id: child.value.id },
query: { name: child.value.name },
})
}
}
</script>
@@ -816,6 +874,7 @@ function goToAssignRewards() {
})
"
:filter-fn="(item) => item.type === 'chore'"
:sort-fn="parentChoreSort"
>
<template #item="{ item }: { item: ChildTask }">
<!-- Kebab menu -->
@@ -983,6 +1042,7 @@ function goToAssignRewards() {
@edit-item="(item) => handleEditItem(item, 'reward')"
@item-ready="handleItemReady"
:getItemClass="(item) => ({ reward: true })"
:sort-fn="parentRewardSort"
>
<template #item="{ item }: { item: RewardStatus }">
<div class="item-name">{{ item.name }}</div>

View File

@@ -1,6 +1,6 @@
<template>
<div class="assign-view">
<h2>Assign Penalties</h2>
<h2>Assign Penalties{{ childName ? ` for ${childName}` : '' }}</h2>
<div class="list-container">
<MessageBlock v-if="countRef === 0" message="No penalties">
<span> <button class="round-btn" @click="goToCreate">Create</button> a penalty </span>
@@ -41,6 +41,7 @@ import { TASK_FIELDS } from '@/common/models'
const route = useRoute()
const router = useRouter()
const childId = route.params.id
const childName = typeof route.query.name === 'string' ? route.query.name : ''
const listRef = ref()
const countRef = ref(-1)

View File

@@ -1,6 +1,6 @@
<template>
<div class="reward-assign-view">
<h2>Assign Rewards</h2>
<h2>Assign Rewards{{ childName ? ` for ${childName}` : '' }}</h2>
<div class="reward-view">
<MessageBlock v-if="rewardCountRef === 0" message="No rewards">
<span> <button class="round-btn" @click="goToCreateReward">Create</button> a reward </span>
@@ -41,6 +41,7 @@ import { REWARD_FIELDS } from '@/common/models'
const route = useRoute()
const router = useRouter()
const childId = route.params.id
const childName = typeof route.query.name === 'string' ? route.query.name : ''
const rewardListRef = ref()
const rewardCountRef = ref(-1)

View File

@@ -52,7 +52,11 @@ const notificationListCountRef = ref(-1)
const refreshKey = ref(0)
function handleNotificationClick(item: PendingConfirmation) {
router.push({ name: 'ParentView', params: { id: item.child_id } })
router.push({
name: 'ParentView',
params: { id: item.child_id },
query: { scrollTo: item.entity_id, entityType: item.entity_type },
})
}
function handleRewardRequest(event: Event) {
@@ -107,6 +111,10 @@ onUnmounted(() => {
justify-content: center;
margin-inline: auto;
align-items: center;
flex-wrap: wrap;
gap: 0.2rem;
max-width: 100%;
overflow: hidden;
}
.child-info {
display: flex;
@@ -114,6 +122,13 @@ onUnmounted(() => {
gap: 0.4rem;
font-weight: 600;
color: var(--dialog-child-name);
min-width: 0;
}
.child-info span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.reward-info {
@@ -123,13 +138,26 @@ onUnmounted(() => {
margin-bottom: 0rem;
font-weight: 600;
color: var(--notification-reward-name);
min-width: 0;
}
.reward-info span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.reward-info img,
.child-info img {
flex-shrink: 0;
}
.requested-text {
margin: 0 0.7rem;
margin: 0 0.4rem;
font-weight: 500;
color: var(--dialog-message);
font-size: 1.05rem;
white-space: nowrap;
flex-shrink: 0;
}
</style>

View File

@@ -10,6 +10,7 @@ const props = defineProps<{
imageFields?: readonly string[]
isParentAuthenticated?: boolean
filterFn?: (item: any) => boolean
sortFn?: (a: any, b: any) => number
getItemClass?: (item: any) => string | string[] | Record<string, boolean>
enableEdit?: boolean
childId?: string
@@ -49,6 +50,7 @@ const fetchItems = async () => {
// Try to use 'tasks', 'reward_status', or 'items' as fallback
let itemList = data[props.itemKey || 'items'] || []
if (props.filterFn) itemList = itemList.filter(props.filterFn)
if (props.sortFn) itemList = [...itemList].sort(props.sortFn)
items.value = itemList
// Fetch images for each item
await Promise.all(
@@ -90,8 +92,19 @@ async function refresh() {
defineExpose({
refresh,
items,
centerItem,
scrollToItem,
})
const scrollToItem = async (itemId: string) => {
await nextTick()
const card = itemRefs.value[itemId]
if (card) {
;(card as HTMLElement).scrollIntoView({ behavior: 'smooth', block: 'nearest' })
}
await centerItem(itemId)
}
const centerItem = async (itemId: string) => {
await nextTick()
const wrapper = scrollWrapper.value