fix: admin booking modals and schedule components polish

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 329eb28ec7
commit db5f6ff135
6 changed files with 104 additions and 36 deletions
@@ -5,17 +5,20 @@
import * as Modal from '$lib/components/ui/dialog'; import * as Modal from '$lib/components/ui/dialog';
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import ApprovalModal from '$lib/components/admin/ApprovalModal.svelte'; import ApprovalModal from '$lib/components/admin/ApprovalModal.svelte';
import RescheduleModal from '$lib/components/admin/RescheduleModal.svelte';
import type { Booking } from '$lib/types/booking'; import type { Booking } from '$lib/types/booking';
interface Props { interface Props {
open: boolean; open: boolean;
bookingId: string; bookingId: string;
onReschedule?: () => void;
} }
let { open = $bindable(), bookingId }: Props = $props(); let { open = $bindable(), bookingId, onReschedule }: Props = $props();
let selectedBooking = $state<Booking | null>(null); let selectedBooking = $state<Booking | null>(null);
let showApprovalModal = $state(false); let showApprovalModal = $state(false);
let showRescheduleModal = $state(false);
// Calculate total duration from services // Calculate total duration from services
// IMPORTANT: Use override_duration_minutes when present — services may have been // IMPORTANT: Use override_duration_minutes when present — services may have been
@@ -131,7 +134,7 @@
</script> </script>
<Modal.Root bind:open> <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> <Modal.Header>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
@@ -302,7 +305,7 @@
</div> </div>
<div> <div>
<div class="text-xs text-gray-500">Email</div> <div class="text-xs text-gray-500">Email</div>
<div class="font-medium break-all">{selectedBooking.user?.email || '—'}</div> <div class="font-medium break-words" title={selectedBooking.user?.email}>{selectedBooking.user?.email || '—'}</div>
</div> </div>
<div> <div>
<div class="text-xs text-gray-500">Phone</div> <div class="text-xs text-gray-500">Phone</div>
@@ -478,6 +481,24 @@
{/if} {/if}
<Modal.Footer class="flex items-center justify-end gap-2"> <Modal.Footer class="flex items-center justify-end gap-2">
{#if selectedBooking && !['completed', 'client_cancelled', 'we_cancelled', 'no_show', 'no_deposit'].includes(selectedBooking.status)}
<Button
variant="outline"
onclick={() => (showRescheduleModal = true)}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="mr-2 h-4 w-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2" />
</svg>
Reschedule
</Button>
{/if}
<Button onclick={() => (open = false)}>Close</Button> <Button onclick={() => (open = false)}>Close</Button>
</Modal.Footer> </Modal.Footer>
</Modal.Content> </Modal.Content>
@@ -494,3 +515,16 @@
}} }}
/> />
{/if} {/if}
<!-- Reschedule Sub-Modal -->
{#if selectedBooking && showRescheduleModal}
<RescheduleModal
bind:open={showRescheduleModal}
booking={selectedBooking}
onSaved={() => {
showRescheduleModal = false;
fetchBookingDetails();
onReschedule?.();
}}
/>
{/if}
@@ -390,7 +390,7 @@
</script> </script>
<Modal.Root bind:open> <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> <Modal.Header>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
@@ -418,7 +418,7 @@
</div> </div>
<div> <div>
<div class="text-xs text-gray-500">Email</div> <div class="text-xs text-gray-500">Email</div>
<div class="font-medium break-all">{booking.user?.email || '—'}</div> <div class="font-medium break-words" title={booking.user?.email}>{booking.user?.email || '—'}</div>
</div> </div>
<div> <div>
<div class="text-xs text-gray-500">Phone</div> <div class="text-xs text-gray-500">Phone</div>
@@ -13,6 +13,8 @@
import * as AlertDialog from '$lib/components/ui/alert-dialog'; import * as AlertDialog from '$lib/components/ui/alert-dialog';
import { Skeleton } from '$lib/components/ui/skeleton'; import { Skeleton } from '$lib/components/ui/skeleton';
const TIME15 = ['00', '15', '30', '45'];
// =============== Types =============== // =============== Types ===============
type WorkingHourRow = { type WorkingHourRow = {
weekday: number; weekday: number;
@@ -527,20 +529,30 @@
<Checkbox bind:checked={row.is_open} /> <Checkbox bind:checked={row.is_open} />
</td> </td>
<td class="py-2"> <td class="py-2">
<Input <select
type="time" bind:value={row.start_time}
bind:value={row.start_time} disabled={!row.is_open}
disabled={!row.is_open} class="w-24 text-sm"
class="w-24 text-sm" >
/> {#each Array(24) as _, hour}
{#each TIME15 as min}
<option value="{String(hour).padStart(2,'0')}:{min}">{String(hour).padStart(2,'0')}:{min}</option>
{/each}
{/each}
</select>
</td> </td>
<td class="py-2"> <td class="py-2">
<Input <select
type="time" bind:value={row.end_time}
bind:value={row.end_time} disabled={!row.is_open}
disabled={!row.is_open} class="w-24 text-sm"
class="w-24 text-sm" >
/> {#each Array(24) as _, hour}
{#each TIME15 as min}
<option value="{String(hour).padStart(2,'0')}:{min}">{String(hour).padStart(2,'0')}:{min}</option>
{/each}
{/each}
</select>
</td> </td>
</tr> </tr>
{/each} {/each}
@@ -225,13 +225,12 @@
}); });
function handleOpenBooking(bookingId: string) { function handleOpenBooking(bookingId: string) {
open = false;
openBookingModal(bookingId); openBookingModal(bookingId);
} }
</script> </script>
<Modal.Root bind:open> <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> <Modal.Header>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
@@ -271,7 +270,7 @@
</div> </div>
<div> <div>
<div class="text-xs text-gray-500">Email</div> <div class="text-xs text-gray-500">Email</div>
<div class="font-medium break-all">{selectedUser.email || '—'}</div> <div class="font-medium break-words" title={selectedUser.email}>{selectedUser.email || '—'}</div>
</div> </div>
<div> <div>
<div class="text-xs text-gray-500">Phone</div> <div class="text-xs text-gray-500">Phone</div>
@@ -280,13 +279,23 @@
<div> <div>
<div class="text-xs text-gray-500">Date of Birth</div> <div class="text-xs text-gray-500">Date of Birth</div>
<div class="font-medium"> <div class="font-medium">
{selectedUser.dateOfBirth {#if selectedUser.dateOfBirth}
? new SvelteDate(selectedUser.dateOfBirth).toLocaleDateString('en-US', { {(() => {
const dob = new SvelteDate(selectedUser.dateOfBirth);
const dateStr = dob.toLocaleDateString('en-US', {
year: 'numeric', year: 'numeric',
month: 'long', month: 'long',
day: 'numeric' day: 'numeric'
}) });
: '—'} const today = new Date();
let age = today.getFullYear() - dob.getFullYear();
const m = today.getMonth() - dob.getMonth();
if (m < 0 || (m === 0 && today.getDate() < dob.getDate())) age--;
return `${dateStr} (${age})`;
})()}
{:else}
{/if}
</div> </div>
</div> </div>
<div> <div>
@@ -308,9 +317,10 @@
<div class="font-medium"> <div class="font-medium">
{#if customerRelationship?.lastVisitDate} {#if customerRelationship?.lastVisitDate}
{new SvelteDate(customerRelationship.lastVisitDate).toLocaleDateString('en-US', { {new SvelteDate(customerRelationship.lastVisitDate).toLocaleDateString('en-US', {
year: 'numeric', weekday: 'short',
day: 'numeric',
month: 'short', month: 'short',
day: 'numeric' year: 'numeric'
})} })}
{:else} {:else}
@@ -324,7 +334,7 @@
<img <img
src={selectedUser.profilePicUrl} src={selectedUser.profilePicUrl}
alt="Profile" alt="Profile"
class="mt-2 h-24 w-24 rounded-lg object-cover" class="mt-2 h-20 w-20 md:h-24 md:w-24 rounded-lg object-cover"
/> />
</div> </div>
{/if} {/if}
@@ -373,6 +373,7 @@
if (res.ok) { if (res.ok) {
toast.success('Booking created successfully!'); toast.success('Booking created successfully!');
open = false; open = false;
window.dispatchEvent(new CustomEvent('bookingApproved'));
onBookingCreated?.(); onBookingCreated?.();
} else { } else {
const errorText = await res.text(); const errorText = await res.text();
@@ -413,7 +414,7 @@
</script> </script>
<Modal.Root bind:open> <Modal.Root bind:open>
<Modal.Content class="max-h-[90vh] max-w-4xl overflow-y-auto"> <Modal.Content class="max-h-[90vh] max-w-[calc(100%-2rem)] overflow-y-auto sm:max-w-2xl md:max-w-4xl">
<Modal.Header> <Modal.Header>
<Modal.Title>Walk-In Booking</Modal.Title> <Modal.Title>Walk-In Booking</Modal.Title>
<Modal.Description> <Modal.Description>
@@ -724,7 +725,7 @@
{#if serviceOverrides[service.id]} {#if serviceOverrides[service.id]}
<div class="rounded-lg border bg-white p-4"> <div class="rounded-lg border bg-white p-4">
<div class="mb-3 font-medium">{service.name}</div> <div class="mb-3 font-medium">{service.name}</div>
<div class="grid grid-cols-2 gap-4"> <div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="space-y-2"> <div class="space-y-2">
<Label for="price-{service.id}" class="text-xs text-gray-600" <Label for="price-{service.id}" class="text-xs text-gray-600"
>Price (£)</Label >Price (£)</Label
@@ -6,12 +6,13 @@
// shadcn-svelte components // shadcn-svelte components
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import * as Card from '$lib/components/ui/card'; import * as Card from '$lib/components/ui/card';
import { Input } from '$lib/components/ui/input';
import { Checkbox } from '$lib/components/ui/checkbox'; import { Checkbox } from '$lib/components/ui/checkbox';
import * as Modal from '$lib/components/ui/dialog'; import * as Modal from '$lib/components/ui/dialog';
import * as AlertDialog from '$lib/components/ui/alert-dialog'; import * as AlertDialog from '$lib/components/ui/alert-dialog';
import { Skeleton } from '$lib/components/ui/skeleton'; import { Skeleton } from '$lib/components/ui/skeleton';
const TIME15 = ['00', '15', '30', '45'];
// =============== Types =============== // =============== Types ===============
type WorkingHourRow = { type WorkingHourRow = {
weekday: number; weekday: number;
@@ -479,20 +480,30 @@
<Checkbox bind:checked={row.is_open} /> <Checkbox bind:checked={row.is_open} />
</td> </td>
<td class="py-2"> <td class="py-2">
<Input <select
type="time"
bind:value={row.start_time} bind:value={row.start_time}
disabled={!row.is_open} disabled={!row.is_open}
class="max-w-[70px] text-sm" class="max-w-[70px] text-sm"
/> >
{#each Array(24) as _, hour}
{#each TIME15 as min}
<option value="{String(hour).padStart(2,'0')}:{min}">{String(hour).padStart(2,'0')}:{min}</option>
{/each}
{/each}
</select>
</td> </td>
<td class="py-2"> <td class="py-2">
<Input <select
type="time"
bind:value={row.end_time} bind:value={row.end_time}
disabled={!row.is_open} disabled={!row.is_open}
class="max-w-[70px] text-sm" class="max-w-[70px] text-sm"
/> >
{#each Array(24) as _, hour}
{#each TIME15 as min}
<option value="{String(hour).padStart(2,'0')}:{min}">{String(hour).padStart(2,'0')}:{min}</option>
{/each}
{/each}
</select>
</td> </td>
</tr> </tr>
{/each} {/each}