WIP call in booking

This commit is contained in:
2026-01-24 22:08:36 +00:00
parent 3a8ea4c98f
commit 2ace6d4d87
6 changed files with 330 additions and 41 deletions
@@ -0,0 +1,108 @@
<script lang="ts">
import { authStore } from '$lib/stores/auth.svelte';
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 { Textarea } from '$lib/components/ui/textarea';
import { SvelteDate } from 'svelte/reactivity';
export let open = false;
export let initialUserId: string | null = null;
let userId = initialUserId ?? '';
let startTime = new SvelteDate().toISOString().slice(0, 16); // datetime-local
let serviceIds: string[] = [];
let notes = '';
// You likely already have this elsewhere
let services: Array<{ id: string; name: string }> = [];
async function loadServices() {
const res = await fetch('/api/services');
if (res.ok) services = await res.json();
}
async function submit() {
if (!userId || serviceIds.length === 0) {
toast.error('User and at least one service are required');
return;
}
const res = await fetch('/api/admin/bookings', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
},
body: JSON.stringify({
user_id: userId,
start_time: new Date(startTime).toISOString(),
service_ids: serviceIds,
notes
})
});
if (!res.ok) {
const text = await res.text();
toast.error(text || 'Failed to create booking');
return;
}
toast.success('Booking created and confirmed');
open = false;
}
$: if (open) loadServices();
</script>
<Modal.Root bind:open>
<Modal.Content class="max-w-lg">
<Modal.Header>
<Modal.Title>Create Booking</Modal.Title>
</Modal.Header>
<div class="space-y-4 p-4">
<div>
<label class="text-xs text-gray-500">User ID</label>
<Input bind:value={userId} placeholder="Paste or search user ID" />
</div>
<div>
<label class="text-xs text-gray-500">Start Time</label>
<Input type="datetime-local" bind:value={startTime} />
</div>
<div>
<label class="text-xs text-gray-500">Services</label>
<div class="space-y-2">
{#each services as service}
<label class="flex items-center gap-2 text-sm">
<input
type="checkbox"
value={service.id}
onchange={(e) => {
const checked = e.currentTarget.checked;
serviceIds = checked
? [...serviceIds, service.id]
: serviceIds.filter((id) => id !== service.id);
}}
/>
{service.name}
</label>
{/each}
</div>
</div>
<div>
<label class="text-xs text-gray-500">Staff Notes</label>
<Textarea rows={3} bind:value={notes} />
</div>
</div>
<Modal.Footer class="flex justify-end gap-2">
<Button variant="outline" onclick={() => (open = false)}>Cancel</Button>
<Button onclick={submit}>Create & Confirm</Button>
</Modal.Footer>
</Modal.Content>
</Modal.Root>
@@ -0,0 +1,35 @@
<script lang="ts">
import { authStore } from '$lib/stores/auth.svelte';
import { toast } from 'svelte-sonner';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Textarea } from '$lib/components/ui/textarea';
import { SvelteDate } from 'svelte/reactivity';
import BookingCreateModal from '$lib/components/admin/BookingCreateModal.svelte';
let showCreateModal = false;
let selectedUserId: string | null = null;
function openForUser(userId: string) {
selectedUserId = userId;
showCreateModal = true;
}
</script>
<div class="rounded-lg border border-gray-200 bg-white p-4">
<h3 class="mb-2 text-sm font-semibold tracking-wide text-gray-600 uppercase">
Call-In / Walk-In Booking
</h3>
<p class="mb-4 text-sm text-gray-500">
Create and confirm a booking immediately while speaking with the client.
</p>
<div class="flex items-center gap-3">
<Button onclick={() => (showCreateModal = true)}>Create Booking</Button>
</div>
</div>
{#if showCreateModal}
<BookingCreateModal bind:open={showCreateModal} initialUserId={selectedUserId} />
{/if}
@@ -24,6 +24,12 @@
duration_minutes: number;
};
function isPastAppointment(startTime: string, durationMinutes: number): boolean {
const start = new Date(startTime);
const end = new Date(start.getTime() + durationMinutes * 60_000);
return end.getTime() < Date.now();
}
let appointments = $state<TodayAppointment[]>([]);
let loading = $state(true);
@@ -179,7 +185,8 @@
<div class="space-y-3">
{#each appointments as apt (apt.id)}
<div
class="flex items-center gap-4 rounded-lg border p-3 transition-all hover:shadow-md"
class="flex items-center gap-4 rounded-lg border p-3 transition-all hover:shadow-md
{isPastAppointment(apt.start_time, apt.duration_minutes) ? 'line-through opacity-50' : ''}"
>
<div class="min-w-[80px] text-sm font-semibold text-gray-700">
{formatTime(apt.start_time)}