fix: two-row footer layout, use DatePicker for reschedule

- Footer: row 1 has Cancel + Reschedule, row 2 has Add to Calendar + Close
- Replace native datetime-local input with DatePicker component from BookingFlow
- Add native time input for preferred time selection
- Buttons use flex-1 for equal width on mobile
This commit is contained in:
2026-05-08 22:13:13 +01:00
parent fa10134d41
commit 8b539afccc
@@ -1,12 +1,13 @@
<script lang="ts">
import { authStore } from '$lib/stores/auth.svelte';
import { SvelteDate } from 'svelte/reactivity';
import { CalendarDate, getLocalTimeZone, type DateValue } from '@internationalized/date';
import { toast } from 'svelte-sonner';
import * as Modal from '$lib/components/ui/dialog';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import * as Textarea from '$lib/components/ui/textarea';
import * as Label from '$lib/components/ui/label';
import DatePicker from '$lib/components/booking/DatePicker.svelte';
import type { Booking } from '$lib/types/booking';
interface Props {
@@ -23,10 +24,18 @@
let cancelling = $state(false);
let showRescheduleForm = $state(false);
let rescheduleDate = $state<CalendarDate | undefined>(undefined);
let rescheduleTime = $state('');
let rescheduleNotes = $state('');
let rescheduleSubmitting = $state(false);
const today = new Date();
const minDate = new CalendarDate(today.getFullYear(), today.getMonth() + 1, today.getDate());
const maxDate = new Date();
maxDate.setMonth(today.getMonth() + 6);
const maxCalendarDate = new CalendarDate(maxDate.getFullYear(), maxDate.getMonth() + 1, maxDate.getDate());
let reschedulePlaceholder = $state<CalendarDate>(minDate);
let totalDuration = $derived(
selectedBooking?.services?.reduce((sum, service) => sum + (service.duration_minutes || 0), 0) || 0
);
@@ -51,6 +60,10 @@
.reduce((sum, p) => sum + p.amount, 0) || 0
);
let isRescheduleValid = $derived(
rescheduleDate && rescheduleTime && rescheduleTime.length >= 4
);
async function fetchBookingDetails() {
if (!bookingId) return;
loading = true;
@@ -86,8 +99,10 @@
selectedBooking = null;
showCancelConfirm = false;
showRescheduleForm = false;
rescheduleDate = undefined;
rescheduleTime = '';
rescheduleNotes = '';
reschedulePlaceholder = minDate;
}, 200);
} else if (bookingId && !selectedBooking) {
fetchBookingDetails();
@@ -123,12 +138,24 @@
}
}
function isDateUnavailable(date: DateValue): boolean {
const d = date as CalendarDate;
const jsDate = d.toDate(getLocalTimeZone());
const now = new Date();
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
return jsDate < todayStart;
}
async function submitReschedule() {
if (!selectedBooking || !rescheduleTime) return;
if (!selectedBooking || !rescheduleDate || !rescheduleTime) return;
rescheduleSubmitting = true;
try {
const [hours, minutes] = rescheduleTime.split(':').map(Number);
const bookingDate = rescheduleDate.toDate(getLocalTimeZone());
bookingDate.setHours(hours || 0, minutes || 0, 0, 0);
const body: Record<string, unknown> = {
new_start_time: new Date(rescheduleTime).toISOString()
new_start_time: bookingDate.toISOString()
};
if (rescheduleNotes.trim()) {
body.notes = rescheduleNotes.trim();
@@ -146,6 +173,7 @@
if (response.ok) {
toast.success('Reschedule request sent — we\'ll confirm shortly');
showRescheduleForm = false;
rescheduleDate = undefined;
rescheduleTime = '';
rescheduleNotes = '';
} else {
@@ -403,15 +431,28 @@
<h3 class="mb-3 text-sm font-semibold tracking-wide text-amber-700 uppercase">
Request Reschedule
</h3>
<div class="space-y-3">
<div class="space-y-2">
<Label.Root for="reschedule-time">New Date & Time *</Label.Root>
<Input
id="reschedule-time"
type="datetime-local"
bind:value={rescheduleTime}
<div class="space-y-4">
<DatePicker
date={rescheduleDate}
placeholder={reschedulePlaceholder}
minValue={minDate}
maxValue={maxCalendarDate}
isDateUnavailable={isDateUnavailable}
onchange={(d) => { rescheduleDate = d; }}
onPlaceholderChange={(p) => { reschedulePlaceholder = p; }}
/>
<div class="space-y-2">
<Label.Root for="reschedule-time">Preferred Time *</Label.Root>
<input
id="reschedule-time"
type="time"
bind:value={rescheduleTime}
class="flex h-9 w-full rounded-md border border-gray-300 bg-transparent px-3 py-1 text-sm shadow-sm"
/>
<p class="text-xs text-gray-500">We'll confirm the exact time based on availability</p>
</div>
<div class="space-y-2">
<Label.Root for="reschedule-notes">Reason (optional)</Label.Root>
<Textarea.Root
@@ -421,11 +462,12 @@
rows={2}
/>
</div>
<div class="flex gap-2">
<Button
size="sm"
onclick={submitReschedule}
disabled={!rescheduleTime || rescheduleSubmitting}
disabled={!isRescheduleValid || rescheduleSubmitting}
>
{rescheduleSubmitting ? 'Submitting...' : 'Submit Request'}
</Button>
@@ -434,6 +476,7 @@
variant="outline"
onclick={() => {
showRescheduleForm = false;
rescheduleDate = undefined;
rescheduleTime = '';
rescheduleNotes = '';
}}
@@ -447,26 +490,25 @@
</div>
{/if}
<Modal.Footer class="flex flex-wrap items-center justify-between gap-2">
<Modal.Footer class="flex flex-col gap-2">
<div class="flex gap-2">
{#if isCancellable}
<Button
variant="destructive"
size="sm"
class="flex-1"
onclick={() => (showCancelConfirm = true)}
>
Cancel Booking
</Button>
{/if}
</div>
<div class="flex gap-2">
{#if isCancellable}
<Button
variant="outline"
size="sm"
class="flex-1"
onclick={() => {
showRescheduleForm = !showRescheduleForm;
if (!showRescheduleForm) {
rescheduleDate = undefined;
rescheduleTime = '';
rescheduleNotes = '';
}
@@ -475,10 +517,13 @@
{showRescheduleForm ? 'Hide Reschedule' : 'Reschedule'}
</Button>
{/if}
</div>
<div class="flex gap-2">
{#if selectedBooking}
<Button
variant="outline"
size="sm"
class="flex-1"
onclick={() => {
if (selectedBooking) {
window.open(`/api/bookings/${selectedBooking.id}/calendar`, '_blank');
@@ -488,7 +533,7 @@
Add to Calendar
</Button>
{/if}
<Button size="sm" onclick={() => (open = false)}>Close</Button>
<Button size="sm" class="flex-1" onclick={() => (open = false)}>Close</Button>
</div>
</Modal.Footer>
</Modal.Content>