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