From fff8afcbe923aa2db9ecd95df45773a003244cfb Mon Sep 17 00:00:00 2001 From: Stephen Adamson Date: Thu, 28 May 2026 16:32:15 +0100 Subject: [PATCH] feat: account edit request modal with auto-select and responsive improvements Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- .../account/EditRequestModal.svelte | 83 ++++++++++++++++--- .../account/UserBookingModal.svelte | 6 +- frontend/src/routes/account/+page.svelte | 52 +++++++----- 3 files changed, 105 insertions(+), 36 deletions(-) diff --git a/frontend/src/lib/components/account/EditRequestModal.svelte b/frontend/src/lib/components/account/EditRequestModal.svelte index a056e15..aa4d1ec 100644 --- a/frontend/src/lib/components/account/EditRequestModal.svelte +++ b/frontend/src/lib/components/account/EditRequestModal.svelte @@ -59,6 +59,7 @@ Record }> >(); let userNavigatedCalendar = $state(false); + let editRequestAutoSelectDone = $state(false); // ─── Date constants ───────────────────────────────────── const today = new Date(); const minDate = new CalendarDate(today.getFullYear(), today.getMonth() + 1, today.getDate()); @@ -187,12 +188,14 @@ for (const slot of dayAH.slots) { const [sh, sm] = slot.startTime.split(':').map(Number); const [eh, em] = slot.endTime.split(':').map(Number); - let startMin = sh * 60 + sm; + // Round up to next 15-min boundary so generated times align with + // the 15-min grid from working hours start used in generateGroupedTimeSlots + let startMin = Math.ceil((sh * 60 + sm) / 15) * 15; const endMin = eh * 60 + em; if (isToday) { const currentMin = now.getHours() * 60 + now.getMinutes(); - startMin = Math.max(startMin, currentMin + 120); + startMin = Math.max(startMin, Math.ceil((currentMin + 60) / 15) * 15); } for (let m = startMin; m < endMin; m += 15) { @@ -240,7 +243,7 @@ const isToday = date.compare(todayCal) === 0; if (isToday) { const currentMin = now.getHours() * 60 + now.getMinutes(); - startMin = Math.max(startMin, currentMin + 120); + startMin = Math.max(startMin, Math.ceil((currentMin + 60) / 15) * 15); } const availableSlots = generateAvailableTimeSlots(duration, date); @@ -360,6 +363,7 @@ notes = originalNotes; submitting = false; initializeHours(); + editRequestAutoSelectDone = false; placeholderDate = minDate; fetchHoursRange(minDate, 3); } @@ -384,6 +388,60 @@ } }); + // ─── Auto-select ──────────────────────────────────────── + $effect(() => { + if (!workingHours || !availableHours || newDate || userNavigatedCalendar || editRequestAutoSelectDone) return; + editRequestAutoSelectDone = true; + + const currentDate = new SvelteDate(); + const maxDateJs = new SvelteDate( + maxCalendarDate.year, + maxCalendarDate.month - 1, + maxCalendarDate.day + ); + + const daysDifference = Math.floor( + (maxDateJs.getTime() - currentDate.getTime()) / (1000 * 60 * 60 * 24) + ); + const daysToCheck = Math.min(daysDifference, 180); + + for (let i = 0; i <= daysToCheck; i++) { + const nextDate = new SvelteDate(currentDate); + nextDate.setDate(currentDate.getDate() + i); + const dateStr = nextDate.toISOString().split('T')[0]; + + if (workingHours[dateStr]?.isOpen) { + const calDate = new CalendarDate( + nextDate.getFullYear(), + nextDate.getMonth() + 1, + nextDate.getDate() + ); + if (!isDateUnavailable(calDate)) { + newDate = calDate; + if (!userNavigatedCalendar) { + placeholderDate = new CalendarDate( + nextDate.getFullYear(), + nextDate.getMonth() + 1, + 1 + ); + } + return; + } + } + } + + const tomorrow = new SvelteDate(); + tomorrow.setDate(tomorrow.getDate() + 1); + newDate = new CalendarDate( + tomorrow.getFullYear(), + tomorrow.getMonth() + 1, + tomorrow.getDate() + ); + if (!userNavigatedCalendar) { + placeholderDate = new CalendarDate(tomorrow.getFullYear(), tomorrow.getMonth() + 1, 1); + } + }); + // ─── API calls ────────────────────────────────────────── async function fetchHoursRange(startDate: CalendarDate, months: number) { loadingHours = true; @@ -436,8 +494,9 @@ availableHoursCache.set(key, ahMap); } - workingHours = whMap; - availableHours = ahMap; + // MERGE instead of replace — preserves data from previously loaded months + workingHours = { ...(workingHours || {}), ...whMap }; + availableHours = { ...(availableHours || {}), ...ahMap }; } } catch (err) { console.error('Failed to fetch hours:', err); @@ -449,8 +508,11 @@ async function fetchHoursForMonth(date: CalendarDate) { const monthKey = `${date.year}-${String(date.month).padStart(2, '0')}`; if (workingHoursCache.has(monthKey) && availableHoursCache.has(monthKey)) { - workingHours = workingHoursCache.get(monthKey)!; - availableHours = availableHoursCache.get(monthKey)!; + const cachedWH = workingHoursCache.get(monthKey)!; + const cachedAH = availableHoursCache.get(monthKey)!; + // MERGE instead of replace — preserves data from other loaded months + workingHours = { ...(workingHours || {}), ...cachedWH }; + availableHours = { ...(availableHours || {}), ...cachedAH }; return; } @@ -489,8 +551,9 @@ workingHoursCache.set(monthKey, whMap); availableHoursCache.set(monthKey, ahMap); - workingHours = whMap; - availableHours = ahMap; + // MERGE instead of replace — preserves data from other loaded months + workingHours = { ...(workingHours || {}), ...whMap }; + availableHours = { ...(availableHours || {}), ...ahMap }; } } catch (err) { console.error('Failed to fetch hours:', err); @@ -653,7 +716,7 @@ - +
{modalTitle()} diff --git a/frontend/src/lib/components/account/UserBookingModal.svelte b/frontend/src/lib/components/account/UserBookingModal.svelte index 1d4f59f..3cf18c9 100644 --- a/frontend/src/lib/components/account/UserBookingModal.svelte +++ b/frontend/src/lib/components/account/UserBookingModal.svelte @@ -249,7 +249,7 @@ - +
@@ -584,7 +584,7 @@ {/if} (showCancelConfirm = v)}> - + Cancel Booking @@ -622,7 +622,7 @@ } }} > - + Leave a Tip Show your appreciation for great service diff --git a/frontend/src/routes/account/+page.svelte b/frontend/src/routes/account/+page.svelte index 38155ae..1b43814 100644 --- a/frontend/src/routes/account/+page.svelte +++ b/frontend/src/routes/account/+page.svelte @@ -769,14 +769,14 @@