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:
2026-06-18 16:26:58 +01:00
co-authored by Sisyphus
parent 165d3d6c38
commit b23a8b89c4
16 changed files with 370 additions and 121 deletions
@@ -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">