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>
+29 -23
View File
@@ -769,14 +769,14 @@
<div class="desktop-tab-menu">
<div class="flex rounded-lg border bg-gray-50 p-1">
<button
class="flex-1 rounded-md px-4 py-2 text-sm font-medium transition-colors {activeTab ===
class="flex-1 rounded-md px-2 py-2 text-xs font-medium transition-colors {activeTab ===
'general'
? 'bg-white text-gray-900 shadow-sm'
: 'text-gray-600 hover:text-gray-900'}"
onclick={(_) => (activeTab = 'general')}
>
<svg
class="mx-auto mb-1 h-5 w-5"
class="mx-auto mb-1 h-4 w-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
@@ -788,14 +788,14 @@
General
</button>
<button
class="flex-1 rounded-md px-4 py-2 text-sm font-medium transition-colors {activeTab ===
class="flex-1 rounded-md px-2 py-2 text-xs font-medium transition-colors {activeTab ===
'history'
? 'bg-white text-gray-900 shadow-sm'
: 'text-gray-600 hover:text-gray-900'}"
onclick={(_) => (activeTab = 'history')}
>
<svg
class="mx-auto mb-1 h-5 w-5"
class="mx-auto mb-1 h-4 w-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
@@ -809,14 +809,14 @@
History
</button>
<button
class="flex-1 rounded-md px-4 py-2 text-sm font-medium transition-colors {activeTab ===
class="flex-1 rounded-md px-2 py-2 text-xs font-medium transition-colors {activeTab ===
'referral'
? 'bg-white text-gray-900 shadow-sm'
: 'text-gray-600 hover:text-gray-900'}"
onclick={(_) => (activeTab = 'referral')}
>
<svg
class="mx-auto mb-1 h-5 w-5"
class="mx-auto mb-1 h-4 w-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
@@ -831,7 +831,7 @@
</button>
{#if canSaveCards}
<button
class="flex-1 rounded-md px-4 py-2 text-sm font-medium transition-colors {activeTab ===
class="flex-1 rounded-md px-2 py-2 text-xs font-medium transition-colors {activeTab ===
'cards'
? 'bg-white text-gray-900 shadow-sm'
: 'text-gray-600 hover:text-gray-900'}"
@@ -841,7 +841,7 @@
}}
>
<svg
class="mx-auto mb-1 h-5 w-5"
class="mx-auto mb-1 h-4 w-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
@@ -854,14 +854,14 @@
</button>
{/if}
<button
class="flex-1 rounded-md px-4 py-2 text-sm font-medium transition-colors {activeTab ===
class="flex-1 rounded-md px-2 py-2 text-xs font-medium transition-colors {activeTab ===
'admin'
? 'bg-white text-gray-900 shadow-sm'
: 'text-gray-600 hover:text-gray-900'}"
onclick={(_) => (activeTab = 'admin')}
>
<svg
class="mx-auto mb-1 h-5 w-5"
class="mx-auto mb-1 h-4 w-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
@@ -1221,7 +1221,7 @@
<div
class="mb-4 flex items-baseline justify-center space-x-2
text-4xl font-bold tracking-wider text-slate-900"
text-2xl break-all font-bold tracking-wider text-slate-900 sm:text-4xl"
>
{#if userData.referralCode}
{#each userData.referralCode.match(/.{1,4}/g) as part (part)}
@@ -1557,14 +1557,14 @@
<div class="mobile-tab-menu">
<div class="flex rounded-lg border bg-gray-50 p-1">
<button
class="flex-1 rounded-md px-4 py-2 text-sm font-medium transition-colors {activeTab ===
class="flex-1 rounded-md px-2 py-2 text-xs font-medium transition-colors {activeTab ===
'general'
? 'bg-white text-gray-900 shadow-sm'
: 'text-gray-600 hover:text-gray-900'}"
onclick={(_) => (activeTab = 'general')}
>
<svg
class="mx-auto mb-1 h-5 w-5"
class="mx-auto mb-1 h-4 w-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
@@ -1577,14 +1577,14 @@
</button>
<button
class="flex-1 rounded-md px-4 py-2 text-sm font-medium transition-colors {activeTab ===
class="flex-1 rounded-md px-2 py-2 text-xs font-medium transition-colors {activeTab ===
'history'
? 'bg-white text-gray-900 shadow-sm'
: 'text-gray-600 hover:text-gray-900'}"
onclick={(_) => (activeTab = 'history')}
>
<svg
class="mx-auto mb-1 h-5 w-5"
class="mx-auto mb-1 h-4 w-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
@@ -1599,14 +1599,14 @@
</button>
<button
class="flex-1 rounded-md px-4 py-2 text-sm font-medium transition-colors {activeTab ===
class="flex-1 rounded-md px-2 py-2 text-xs font-medium transition-colors {activeTab ===
'referral'
? 'bg-white text-gray-900 shadow-sm'
: 'text-gray-600 hover:text-gray-900'}"
onclick={(_) => (activeTab = 'referral')}
>
<svg
class="mx-auto mb-1 h-5 w-5"
class="mx-auto mb-1 h-4 w-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
@@ -1622,14 +1622,14 @@
{#if canSaveCards}
<button
class="flex-1 rounded-md px-4 py-2 text-sm font-medium transition-colors {activeTab ===
'cards'
class="flex-1 rounded-md px-2 py-2 text-xs font-medium transition-colors {activeTab ===
'general'
? 'bg-white text-gray-900 shadow-sm'
: 'text-gray-600 hover:text-gray-900'}"
onclick={(_) => (activeTab = 'cards')}
onclick={(_) => (activeTab = 'general')}
>
<svg
class="mx-auto mb-1 h-5 w-5"
class="mx-auto mb-1 h-4 w-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
@@ -1643,14 +1643,14 @@
{/if}
<button
class="flex-1 rounded-md px-4 py-2 text-sm font-medium transition-colors {activeTab ===
class="flex-1 rounded-md px-2 py-2 text-xs font-medium transition-colors {activeTab ===
'admin'
? 'bg-white text-gray-900 shadow-sm'
: 'text-gray-600 hover:text-gray-900'}"
onclick={(_) => (activeTab = 'admin')}
>
<svg
class="mx-auto mb-1 h-5 w-5"
class="mx-auto mb-1 h-4 w-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
@@ -1815,6 +1815,12 @@
right: 0;
z-index: 50;
display: block;
padding-bottom: env(safe-area-inset-bottom, 0);
}
.mobile-tab-menu > div {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.desktop-tab-menu {