Refactor Time Selector and Scheduler UI; Implement TimePickerPopover Component
Some checks failed
Chore App Build, Test, and Push Docker Images / build-and-push (push) Failing after 1m5s

- Updated TimeSelector.vue styles for smaller dimensions and font sizes.
- Added new API proxy for '/events' in vite.config.ts.
- Created bug specifications for various UI issues and fixes in bugs-1.0.5-001.md and bugs-1.0.5-002.md.
- Introduced TimePickerPopover.vue for a new time selection interface in the chore scheduler.
- Refactored ScheduleModal.vue to replace checkbox rows with a chip-based design for selecting specific days.
- Enhanced chore scheduling logic to ensure proper handling of time extensions and UI updates.
This commit is contained in:
2026-02-25 19:45:31 -05:00
parent a41a357f50
commit 91a52c1973
19 changed files with 1250 additions and 176 deletions

View File

@@ -72,6 +72,9 @@ const pendingEditOverrideTarget = ref<{ entity: Task | Reward; type: 'task' | 'r
// Kebab menu
const activeMenuFor = ref<string | null>(null)
const shouldIgnoreNextCardClick = ref(false)
const selectedChoreId = ref<string | null>(null)
const menuPosition = ref({ top: 0, left: 0 })
const kebabBtnRefs = ref<Map<string, HTMLElement>>(new Map())
// Schedule modal
const showScheduleModal = ref(false)
@@ -85,6 +88,12 @@ const lastFetchDate = ref<string>(toLocalISODate(new Date()))
function handleItemReady(itemId: string) {
readyItemId.value = itemId
selectedChoreId.value = null
}
function handleChoreItemReady(itemId: string) {
readyItemId.value = itemId
selectedChoreId.value = itemId || null
}
function handleTaskTriggered(event: Event) {
@@ -247,7 +256,7 @@ function handleChoreScheduleModified(event: Event) {
const payload = event.payload as ChoreScheduleModifiedPayload
if (child.value && payload.child_id === child.value.id) {
childChoreListRef.value?.refresh()
resetExpiryTimers()
setTimeout(() => resetExpiryTimers(), 300)
}
}
@@ -255,7 +264,7 @@ function handleChoreTimeExtended(event: Event) {
const payload = event.payload as ChoreTimeExtendedPayload
if (child.value && payload.child_id === child.value.id) {
childChoreListRef.value?.refresh()
resetExpiryTimers()
setTimeout(() => resetExpiryTimers(), 300)
}
}
@@ -274,6 +283,7 @@ const onDocClick = (e: MouseEvent) => {
})
if (!inside) {
activeMenuFor.value = null
selectedChoreId.value = null
if (path.some((n) => n instanceof HTMLElement && n.classList.contains('item-card'))) {
shouldIgnoreNextCardClick.value = true
}
@@ -283,6 +293,11 @@ const onDocClick = (e: MouseEvent) => {
function openChoreMenu(taskId: string, e: MouseEvent) {
e.stopPropagation()
const btn = kebabBtnRefs.value.get(taskId)
if (btn) {
const rect = btn.getBoundingClientRect()
menuPosition.value = { top: rect.bottom, left: rect.right - 140 }
}
activeMenuFor.value = taskId
}
@@ -302,8 +317,7 @@ function openScheduleModal(item: ChildTask, e: MouseEvent) {
function onScheduleSaved() {
showScheduleModal.value = false
scheduleTarget.value = null
childChoreListRef.value?.refresh()
resetExpiryTimers()
setTimeout(() => resetExpiryTimers(), 300)
}
// ── Extend Time ───────────────────────────────────────────────────────────────
@@ -317,8 +331,9 @@ async function doExtendTime(item: ChildTask, e: MouseEvent) {
if (!res.ok) {
const { msg } = await parseErrorResponse(res)
alert(`Error: ${msg}`)
return
}
// SSE chore_time_extended event will trigger a refresh
setTimeout(() => resetExpiryTimers(), 300)
}
// ── Schedule state helpers (for item-slot use) ────────────────────────────────
@@ -667,7 +682,7 @@ function goToAssignRewards() {
:readyItemId="readyItemId"
:isParentAuthenticated="true"
@trigger-item="triggerTask"
@item-ready="handleItemReady"
@item-ready="handleChoreItemReady"
:getItemClass="
(item) => ({
bad: !item.is_good,
@@ -681,7 +696,14 @@ function goToAssignRewards() {
<!-- Kebab menu -->
<div class="chore-kebab-wrap" @click.stop>
<button
v-show="selectedChoreId === item.id"
class="kebab-btn"
:ref="
(el) => {
if (el) kebabBtnRefs.set(item.id, el as HTMLElement)
else kebabBtnRefs.delete(item.id)
}
"
@mousedown.stop.prevent
@click="openChoreMenu(item.id, $event)"
:aria-expanded="activeMenuFor === item.id ? 'true' : 'false'"
@@ -689,31 +711,34 @@ function goToAssignRewards() {
>
</button>
<div
v-if="activeMenuFor === item.id"
class="kebab-menu"
@mousedown.stop.prevent
@click.stop
>
<button class="menu-item" @mousedown.stop.prevent @click="editChorePoints(item)">
Edit Points
</button>
<button
class="menu-item"
<Teleport to="body">
<div
v-if="activeMenuFor === item.id"
class="kebab-menu"
:style="{ top: menuPosition.top + 'px', left: menuPosition.left + 'px' }"
@mousedown.stop.prevent
@click="openScheduleModal(item, $event)"
@click.stop
>
Schedule
</button>
<button
v-if="isChoreExpired(item)"
class="menu-item"
@mousedown.stop.prevent
@click="doExtendTime(item, $event)"
>
Extend Time
</button>
</div>
<button class="menu-item" @mousedown.stop.prevent @click="editChorePoints(item)">
Edit Points
</button>
<button
class="menu-item"
@mousedown.stop.prevent
@click="openScheduleModal(item, $event)"
>
Schedule
</button>
<button
v-if="isChoreExpired(item)"
class="menu-item"
@mousedown.stop.prevent
@click="doExtendTime(item, $event)"
>
Extend Time
</button>
</div>
</Teleport>
</div>
<!-- TOO LATE badge -->
@@ -973,8 +998,20 @@ function goToAssignRewards() {
background: var(--list-item-bg-reward);
}
:deep(.chore-inactive) {
opacity: 0.45;
filter: grayscale(60%);
position: relative;
}
:deep(.chore-inactive::before) {
content: '';
position: absolute;
inset: 0;
background: rgba(160, 160, 160, 0.45);
filter: grayscale(80%);
z-index: 1;
pointer-events: none;
border-radius: inherit;
}
:deep(.chore-inactive) .kebab-btn {
color: rgba(255, 255, 255, 0.85);
}
/* Chore kebab menu (inside item-card which is position:relative in ScrollingList) */
@@ -1006,9 +1043,7 @@ function goToAssignRewards() {
}
.kebab-menu {
position: absolute;
top: 36px;
right: 0;
position: fixed;
min-width: 140px;
background: var(--kebab-menu-bg, #f7fafc);
border: 1.5px solid var(--kebab-menu-border, #bcc1c9);
@@ -1017,7 +1052,7 @@ function goToAssignRewards() {
display: flex;
flex-direction: column;
overflow: hidden;
z-index: 30;
z-index: 9999;
border-radius: 6px;
}
@@ -1043,8 +1078,9 @@ function goToAssignRewards() {
left: 50%;
transform: translate(-50%, -50%);
width: 80%;
background: rgba(34, 34, 43, 0.85);
color: var(--btn-danger, #ef4444);
background: rgba(34, 34, 34, 0.65);
color: var(--text-bad-color, #ef4444);
text-shadow: var(--item-points-shadow);
font-weight: 700;
font-size: 1.05rem;
text-align: center;
@@ -1061,9 +1097,9 @@ function goToAssignRewards() {
/* Due time sub-text */
.due-label {
font-size: 0.72rem;
font-size: 0.85rem;
font-weight: 600;
color: var(--item-points-color, #ffd166);
color: var(--text-bad-color, #ef4444);
margin-top: 0.2rem;
letter-spacing: 0.3px;
}