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