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