chore: apply project-wide formatting and UI library updates
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -74,7 +74,11 @@
|
||||
|
||||
// Step 4: Date & Time
|
||||
let placeholder = $state<CalendarDate>(
|
||||
new CalendarDate(new SvelteDate().getFullYear(), new SvelteDate().getMonth() + 1, new SvelteDate().getDate())
|
||||
new CalendarDate(
|
||||
new SvelteDate().getFullYear(),
|
||||
new SvelteDate().getMonth() + 1,
|
||||
new SvelteDate().getDate()
|
||||
)
|
||||
);
|
||||
let selectedDate = $state<CalendarDate | undefined>(undefined);
|
||||
let selectedTime = $state<string | null>(null);
|
||||
@@ -129,7 +133,7 @@
|
||||
|
||||
// =============== Lunch Protection ===============
|
||||
const lunchProtection = $derived(
|
||||
(selectedDate && selectedServices.length > 0)
|
||||
selectedDate && selectedServices.length > 0
|
||||
? buildLunchProtection(selectedDate, workingHours, availableHours, getTotalDuration(), true)
|
||||
: new Map()
|
||||
);
|
||||
@@ -197,7 +201,16 @@
|
||||
// Auto-select first available date once data loads (timing-safe, data-driven)
|
||||
let bookingCreateAutoSelectDone = $state(false);
|
||||
$effect(() => {
|
||||
if (open && currentStep === 4 && workingHours && availableHours && !selectedDate && selectedServices.length > 0 && !userNavigatedCalendar && !bookingCreateAutoSelectDone) {
|
||||
if (
|
||||
open &&
|
||||
currentStep === 4 &&
|
||||
workingHours &&
|
||||
availableHours &&
|
||||
!selectedDate &&
|
||||
selectedServices.length > 0 &&
|
||||
!userNavigatedCalendar &&
|
||||
!bookingCreateAutoSelectDone
|
||||
) {
|
||||
bookingCreateAutoSelectDone = true;
|
||||
const now = new SvelteDate();
|
||||
const maxDateJs = new SvelteDate(
|
||||
@@ -213,7 +226,11 @@
|
||||
const checkDate = new SvelteDate(now);
|
||||
checkDate.setDate(now.getDate() + i);
|
||||
const dateStr = checkDate.toISOString().split('T')[0];
|
||||
const calDate = new CalendarDate(checkDate.getFullYear(), checkDate.getMonth() + 1, checkDate.getDate());
|
||||
const calDate = new CalendarDate(
|
||||
checkDate.getFullYear(),
|
||||
checkDate.getMonth() + 1,
|
||||
checkDate.getDate()
|
||||
);
|
||||
if (workingHours[dateStr]?.isOpen && !isDateUnavailable(calDate)) {
|
||||
selectedDate = calDate;
|
||||
if (!userNavigatedCalendar) {
|
||||
@@ -342,7 +359,9 @@
|
||||
const whMap: Record<string, any> = {};
|
||||
const ahMap: Record<string, any> = {};
|
||||
|
||||
whData.forEach((d) => (whMap[d.date] = { isOpen: d.isOpen, startTime: d.startTime, endTime: d.endTime }));
|
||||
whData.forEach(
|
||||
(d) => (whMap[d.date] = { isOpen: d.isOpen, startTime: d.startTime, endTime: d.endTime })
|
||||
);
|
||||
ahData.forEach((d) => (ahMap[d.date] = { isOpen: d.isOpen, slots: d.slots }));
|
||||
|
||||
if (gen !== hoursRangeGeneration) return; // Stale response, discard
|
||||
@@ -435,7 +454,7 @@
|
||||
} finally {
|
||||
loadingWorkingHours = false;
|
||||
loadingAvailableHours = false;
|
||||
loadingMonthKeys = new Set([...loadingMonthKeys].filter(k => k !== monthKey));
|
||||
loadingMonthKeys = new Set([...loadingMonthKeys].filter((k) => k !== monthKey));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -575,7 +594,13 @@
|
||||
|
||||
const groupedTimeSlots = $derived(
|
||||
currentStep === 4 && selectedServices.length > 0 && selectedDate
|
||||
? generateGroupedTimeSlots(selectedDate, workingHours, availableHours, getTotalDuration(), lunchProtection)
|
||||
? generateGroupedTimeSlots(
|
||||
selectedDate,
|
||||
workingHours,
|
||||
availableHours,
|
||||
getTotalDuration(),
|
||||
lunchProtection
|
||||
)
|
||||
: []
|
||||
);
|
||||
|
||||
@@ -599,8 +624,20 @@
|
||||
if (duration <= 0) return true;
|
||||
|
||||
// Build lunch protection specifically for the date being checked
|
||||
const dayProtection = buildLunchProtection(date as CalendarDate, workingHours, availableHours, duration, true);
|
||||
const slots = generateAvailableTimeSlots(date as CalendarDate, workingHours, availableHours, duration, dayProtection);
|
||||
const dayProtection = buildLunchProtection(
|
||||
date as CalendarDate,
|
||||
workingHours,
|
||||
availableHours,
|
||||
duration,
|
||||
true
|
||||
);
|
||||
const slots = generateAvailableTimeSlots(
|
||||
date as CalendarDate,
|
||||
workingHours,
|
||||
availableHours,
|
||||
duration,
|
||||
dayProtection
|
||||
);
|
||||
if (slots.length === 0) return true;
|
||||
|
||||
return false;
|
||||
@@ -620,32 +657,32 @@
|
||||
try {
|
||||
let finalUserId = selectedUserId;
|
||||
|
||||
if (userType === 'guest') {
|
||||
const phone = guestPhone.trim() || '+447700900000';
|
||||
const createRes = await fetch('/api/users/guest', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${authStore.currentToken}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
firstName: guestName.trim().split(' ')[0] || 'Guest',
|
||||
lastName: guestName.trim().split(' ').slice(1).join(' ') || 'Customer',
|
||||
phone: phone,
|
||||
email: `callin-${Date.now()}@guest.invalid`
|
||||
})
|
||||
});
|
||||
if (userType === 'guest') {
|
||||
const phone = guestPhone.trim() || '+447700900000';
|
||||
const createRes = await fetch('/api/users/guest', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${authStore.currentToken}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
firstName: guestName.trim().split(' ')[0] || 'Guest',
|
||||
lastName: guestName.trim().split(' ').slice(1).join(' ') || 'Customer',
|
||||
phone: phone,
|
||||
email: `callin-${Date.now()}@guest.invalid`
|
||||
})
|
||||
});
|
||||
|
||||
if (!createRes.ok) {
|
||||
toast.error('Failed to create guest user');
|
||||
submitting = false;
|
||||
return;
|
||||
if (!createRes.ok) {
|
||||
toast.error('Failed to create guest user');
|
||||
submitting = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const guestUser = await createRes.json();
|
||||
finalUserId = guestUser.id;
|
||||
}
|
||||
|
||||
const guestUser = await createRes.json();
|
||||
finalUserId = guestUser.id;
|
||||
}
|
||||
|
||||
if (!finalUserId) throw new Error('User ID required');
|
||||
if (!selectedDate || !selectedTime) throw new Error('Date and time required');
|
||||
|
||||
@@ -734,7 +771,9 @@
|
||||
</script>
|
||||
|
||||
<Modal.Root bind:open>
|
||||
<Modal.Content class="max-h-[90vh] max-w-[calc(100%-2rem)] overflow-y-auto sm:max-w-2xl md:max-w-4xl">
|
||||
<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.Title>Create Admin Booking</Modal.Title>
|
||||
<Modal.Description>
|
||||
@@ -1096,17 +1135,30 @@
|
||||
<Card.Content class="space-y-4 p-0">
|
||||
<!-- Reservation Countdown Banner -->
|
||||
{#if reservationId && reservationExpiresAt}
|
||||
<div class="mx-6 mt-4 rounded-lg bg-green-50 border border-green-200 p-3">
|
||||
<div class="mx-6 mt-4 rounded-lg border border-green-200 bg-green-50 p-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="h-5 w-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
||||
<svg
|
||||
class="h-5 w-5 text-green-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M5 13l4 4L19 7"
|
||||
></path>
|
||||
</svg>
|
||||
<span class="text-sm font-medium text-green-800">
|
||||
Slot reserved until {reservationExpiresAt.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
||||
Slot reserved until {reservationExpiresAt.toLocaleTimeString([], {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<span class="text-sm font-mono font-semibold text-green-700">
|
||||
<span class="font-mono text-sm font-semibold text-green-700">
|
||||
({reservationCountdown} remaining)
|
||||
</span>
|
||||
</div>
|
||||
@@ -1123,11 +1175,11 @@
|
||||
selectedDate = newDate;
|
||||
selectedTime = null;
|
||||
}}
|
||||
onPlaceholderChange={(newPlaceholder) => {
|
||||
placeholder = newPlaceholder;
|
||||
userNavigatedCalendar = true;
|
||||
fetchHoursForMonth(newPlaceholder);
|
||||
}}
|
||||
onPlaceholderChange={(newPlaceholder) => {
|
||||
placeholder = newPlaceholder;
|
||||
userNavigatedCalendar = true;
|
||||
fetchHoursForMonth(newPlaceholder);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1139,14 +1191,18 @@
|
||||
<div class="border-t">
|
||||
<div class="max-h-64 overflow-y-auto p-6">
|
||||
{#if formattedSelectedDate}
|
||||
<div class="mb-3 grid justify-center gap-2 text-sm font-medium">{formattedSelectedDate}</div>
|
||||
<div class="mb-3 grid justify-center gap-2 text-sm font-medium">
|
||||
{formattedSelectedDate}
|
||||
</div>
|
||||
{/if}
|
||||
<TimeSlotList
|
||||
slots={groupedTimeSlots}
|
||||
{selectedTime}
|
||||
duration={getTotalDuration()}
|
||||
protection={lunchProtection}
|
||||
onSelect={(time) => { selectedTime = time; }}
|
||||
onSelect={(time) => {
|
||||
selectedTime = time;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1177,7 +1233,11 @@
|
||||
onclick={submitBooking}
|
||||
class="bg-primary text-primary-foreground"
|
||||
>
|
||||
{isReserving ? 'Reserving Slot...' : submitting ? 'Creating Booking...' : 'Create Booking'}
|
||||
{isReserving
|
||||
? 'Reserving Slot...'
|
||||
: submitting
|
||||
? 'Creating Booking...'
|
||||
: 'Create Booking'}
|
||||
</Button>
|
||||
</Card.Footer>
|
||||
</Card.Root>
|
||||
|
||||
Reference in New Issue
Block a user