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 <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-05-28 16:34:13 +01:00
co-authored by Sisyphus
parent f69750da82
commit fff8afcbe9
3 changed files with 105 additions and 36 deletions
@@ -59,6 +59,7 @@
Record<string, { isOpen: boolean; slots: Array<{ startTime: string; endTime: string }> }>
>();
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 @@
</script>
<Modal.Root bind:open>
<Modal.Content class="max-h-[90vh] max-w-sm overflow-y-auto md:max-w-3xl">
<Modal.Content class="max-h-[90vh] max-w-[calc(100%-2rem)] overflow-y-auto sm:max-w-md md:max-w-3xl">
<Modal.Header>
<div class="flex items-center justify-between">
<Modal.Title class="text-lg font-semibold">{modalTitle()}</Modal.Title>
@@ -249,7 +249,7 @@
</script>
<Modal.Root bind:open>
<Modal.Content class="max-h-[90vh] max-w-sm overflow-y-auto md:max-w-3xl">
<Modal.Content class="max-h-[90vh] max-w-[calc(100%-2rem)] overflow-y-auto sm:max-w-md md:max-w-3xl">
<Modal.Header>
<div class="flex items-center justify-between">
<div>
@@ -584,7 +584,7 @@
{/if}
<Modal.Root open={showCancelConfirm} onOpenChange={(v) => (showCancelConfirm = v)}>
<Modal.Content class="max-w-sm">
<Modal.Content class="max-w-[calc(100%-2rem)]">
<Modal.Header>
<Modal.Title>Cancel Booking</Modal.Title>
<Modal.Description>
@@ -622,7 +622,7 @@
}
}}
>
<Modal.Content class="max-w-sm">
<Modal.Content class="max-w-[calc(100%-2rem)]">
<Modal.Header>
<Modal.Title>Leave a Tip</Modal.Title>
<Modal.Description>Show your appreciation for great service</Modal.Description>