feat(frontend): update admin components
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -3,6 +3,8 @@
|
||||
import { SvelteDate, SvelteSet } from 'svelte/reactivity';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { CalendarDate, getLocalTimeZone, type DateValue } from '@internationalized/date';
|
||||
import { Checkbox } from '$lib/components/ui/checkbox';
|
||||
import PolicyPopover from '$lib/components/ui/policyPopover.svelte';
|
||||
|
||||
import * as Modal from '$lib/components/ui/dialog';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
@@ -65,6 +67,21 @@
|
||||
maxDate.getDate()
|
||||
);
|
||||
|
||||
let hoursUntilAppointment = $derived(
|
||||
(new SvelteDate(booking.start_time).getTime() - new SvelteDate().getTime()) / (1000 * 60 * 60)
|
||||
);
|
||||
let hasPayments = $derived(
|
||||
(booking.amount_paid ?? 0) > 0
|
||||
);
|
||||
let showNoticeWarning = $derived(
|
||||
hasPayments ? hoursUntilAppointment < 72 : hoursUntilAppointment < 24
|
||||
);
|
||||
let showNoShowWarning = $derived(
|
||||
!hasPayments && hoursUntilAppointment < 24 && hoursUntilAppointment >= 0
|
||||
);
|
||||
let forgiveFees = $state(false);
|
||||
let forgiveNoShow = $state(false);
|
||||
|
||||
let bookingDuration = $derived(
|
||||
booking.services?.reduce(
|
||||
(sum, s) => sum + (s.override_duration_minutes ?? s.duration_minutes ?? 0),
|
||||
@@ -305,13 +322,16 @@
|
||||
saving = true;
|
||||
const loadingToast = toast.loading('Rescheduling booking...');
|
||||
try {
|
||||
const body: Record<string, unknown> = { start_time: newStartTime };
|
||||
if (forgiveFees) body.forgive_fees = true;
|
||||
if (forgiveNoShow) body.forgive_noshow = true;
|
||||
const response = await fetch(`/api/admin/bookings/${booking.id}/reschedule`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${authStore.currentToken}`
|
||||
},
|
||||
body: JSON.stringify({ start_time: newStartTime })
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
if (response.ok) {
|
||||
toast.success('Booking rescheduled!', { id: loadingToast });
|
||||
@@ -408,7 +428,7 @@
|
||||
</script>
|
||||
|
||||
<Modal.Root bind:open>
|
||||
<Modal.Content class="!z-[70] max-h-[90vh] max-w-4xl overflow-y-auto">
|
||||
<Modal.Content class="!z-[70] max-h-[90vh] max-w-[calc(100%-2rem)] overflow-y-auto sm:max-w-4xl">
|
||||
<Modal.Header>
|
||||
<Modal.Title class="text-lg font-semibold">Reschedule Booking</Modal.Title>
|
||||
<Modal.Description>
|
||||
@@ -417,6 +437,49 @@
|
||||
</Modal.Header>
|
||||
|
||||
<div class="px-6 pb-4">
|
||||
{#if showNoticeWarning || showNoShowWarning}
|
||||
<div class="mb-4 rounded-md border border-amber-200 bg-amber-50 p-3 text-sm text-amber-800">
|
||||
<p class="font-medium text-amber-900">Short Notice Reschedule</p>
|
||||
|
||||
{#if hasPayments}
|
||||
<p class="mt-1">Rescheduling may forfeit deposit protection on payments made.</p>
|
||||
<details class="mt-1 text-xs text-gray-500">
|
||||
<summary class="cursor-pointer hover:text-gray-700">What this means</summary>
|
||||
<p class="mt-1">"Rescheduling within {Math.round(hoursUntilAppointment)}h of the original time with payments present means deposit protection applies — up to 50% of the total (up to £{(booking.total_amount * 0.5).toFixed(2)}) could be retained depending on notice period."</p>
|
||||
</details>
|
||||
<label class="mt-2 flex items-center gap-2 cursor-pointer">
|
||||
<Checkbox bind:checked={forgiveFees} />
|
||||
<span class="text-xs">Forgive fees — refund fully (overrides deposit protection)</span>
|
||||
</label>
|
||||
<details class="ml-6 text-xs text-gray-500">
|
||||
<summary class="cursor-pointer hover:text-gray-700">What happens with forgiveness</summary>
|
||||
<p class="mt-1">"We've waived deposit protection on this reschedule as a goodwill gesture. The full amount moves to the new appointment instead of having up to 50% retained as deposit."</p>
|
||||
</details>
|
||||
{/if}
|
||||
|
||||
<p class="mt-1">This time change counts as a no-show toward deposit obligations.</p>
|
||||
<details class="mt-1 text-xs text-gray-500">
|
||||
<summary class="cursor-pointer hover:text-gray-700">What this means</summary>
|
||||
<p class="mt-1">"This time change will count as a no-show toward your booking history. Two no-shows within 6 months would require a deposit on future bookings."</p>
|
||||
</details>
|
||||
<label class="mt-2 flex items-center gap-2 cursor-pointer">
|
||||
<Checkbox bind:checked={forgiveNoShow} />
|
||||
<span class="text-xs">Forgive no-show — this reschedule will <strong>not</strong> count toward deposit obligations</span>
|
||||
</label>
|
||||
<details class="ml-6 text-xs text-gray-500">
|
||||
<summary class="cursor-pointer hover:text-gray-700">What happens with forgiveness</summary>
|
||||
<p class="mt-1">"We've waived the no-show record for this reschedule so your deposit obligations are unaffected."</p>
|
||||
</details>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-gray-500">
|
||||
<PolicyPopover>
|
||||
{#snippet trigger()}
|
||||
<span class="underline">View full cancellation policy →</span>
|
||||
{/snippet}
|
||||
</PolicyPopover>
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<Card.Root class="mb-4">
|
||||
<Card.Content class="pt-4">
|
||||
<div class="text-sm font-medium">
|
||||
|
||||
Reference in New Issue
Block a user