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:
@@ -129,11 +129,21 @@
|
||||
if (!wh || !wh.isOpen) return [];
|
||||
const startMin = timeToMinutes(wh.startTime);
|
||||
const endMin = timeToMinutes(wh.endTime);
|
||||
const options: Array<{ hour: string; minute: string; period: 'AM' | 'PM'; totalMin: number; label: string }> = [];
|
||||
const options: Array<{
|
||||
hour: string;
|
||||
minute: string;
|
||||
period: 'AM' | 'PM';
|
||||
totalMin: number;
|
||||
label: string;
|
||||
}> = [];
|
||||
for (let m = startMin; m < endMin; m += 15) {
|
||||
const t = minutesTo12h(m);
|
||||
const isStart = m === startMin;
|
||||
options.push({ ...t, totalMin: m, label: `${t.hour}:${t.minute} ${t.period}${t.period === 'PM' && t.hour === '12' && t.minute === '00' ? ' (noon)' : ''}${isStart ? ' (start of day)' : ''}` });
|
||||
options.push({
|
||||
...t,
|
||||
totalMin: m,
|
||||
label: `${t.hour}:${t.minute} ${t.period}${t.period === 'PM' && t.hour === '12' && t.minute === '00' ? ' (noon)' : ''}${isStart ? ' (start of day)' : ''}`
|
||||
});
|
||||
}
|
||||
return options;
|
||||
});
|
||||
@@ -144,17 +154,25 @@
|
||||
const startMin = timeToMinutes(wh.startTime);
|
||||
const endMin = timeToMinutes(wh.endTime);
|
||||
const currentStartMin = timeToMinutes(to24h(startHour, startMinute, startPeriod));
|
||||
const options: Array<{ hour: string; minute: string; period: 'AM' | 'PM'; totalMin: number; label: string }> = [];
|
||||
const options: Array<{
|
||||
hour: string;
|
||||
minute: string;
|
||||
period: 'AM' | 'PM';
|
||||
totalMin: number;
|
||||
label: string;
|
||||
}> = [];
|
||||
for (let m = Math.max(startMin, currentStartMin + 15); m <= endMin; m += 15) {
|
||||
const t = minutesTo12h(m);
|
||||
const isEnd = m === endMin;
|
||||
options.push({ ...t, totalMin: m, label: `${t.hour}:${t.minute} ${t.period}${t.period === 'PM' && t.hour === '12' && t.minute === '00' ? ' (noon)' : ''}${isEnd ? ' (end of day)' : ''}` });
|
||||
options.push({
|
||||
...t,
|
||||
totalMin: m,
|
||||
label: `${t.hour}:${t.minute} ${t.period}${t.period === 'PM' && t.hour === '12' && t.minute === '00' ? ' (noon)' : ''}${isEnd ? ' (end of day)' : ''}`
|
||||
});
|
||||
}
|
||||
return options;
|
||||
});
|
||||
|
||||
|
||||
|
||||
function formatRelativeTime(iso: string): string {
|
||||
const d = new SvelteDate(iso);
|
||||
const now = new SvelteDate();
|
||||
@@ -174,7 +192,9 @@
|
||||
}
|
||||
|
||||
let sortedBlockers = $derived.by(() => {
|
||||
return [...blockers].sort((a, b) => new SvelteDate(a.start_time).getTime() - new SvelteDate(b.start_time).getTime());
|
||||
return [...blockers].sort(
|
||||
(a, b) => new SvelteDate(a.start_time).getTime() - new SvelteDate(b.start_time).getTime()
|
||||
);
|
||||
});
|
||||
|
||||
let totalPages = $derived.by(() => Math.max(1, Math.ceil(sortedBlockers.length / PAGE_SIZE)));
|
||||
@@ -233,14 +253,21 @@
|
||||
}
|
||||
}
|
||||
|
||||
function buildDateTime(date: string, hour: string, minute: string, period: 'AM' | 'PM'): string | null {
|
||||
function buildDateTime(
|
||||
date: string,
|
||||
hour: string,
|
||||
minute: string,
|
||||
period: 'AM' | 'PM'
|
||||
): string | null {
|
||||
if (!date) return null;
|
||||
const t24 = to24h(hour, minute, period);
|
||||
return `${date}T${t24}:00`;
|
||||
}
|
||||
|
||||
let formComplete = $derived.by(() => {
|
||||
return newDescription.trim() !== '' && newStartDate !== '' && startHour !== '' && endHour !== '';
|
||||
return (
|
||||
newDescription.trim() !== '' && newStartDate !== '' && startHour !== '' && endHour !== ''
|
||||
);
|
||||
});
|
||||
|
||||
let canCreate = $derived.by(() => {
|
||||
@@ -513,128 +540,135 @@
|
||||
<Skeleton class="h-16 w-full" />
|
||||
{/each}
|
||||
</div>
|
||||
{:else if blockers.length === 0}
|
||||
<p class="text-sm text-gray-500">No time blockers found.</p>
|
||||
{:else}
|
||||
{#if blockers.length === 0}
|
||||
<p class="text-sm text-gray-500">No time blockers found.</p>
|
||||
{:else}
|
||||
<div class="space-y-3">
|
||||
{#each pagedBlockers as b (b.id)}
|
||||
<div class="flex items-center justify-between gap-3 rounded-lg border p-3 sm:p-4 transition-all hover:shadow-sm">
|
||||
<div class="flex min-w-0 flex-1 items-center gap-3">
|
||||
<div class="hidden shrink-0 rounded-lg bg-gray-50 p-1.5 sm:block">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4 text-gray-600"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<rect x="3" y="4" width="18" height="18" rx="2" ry="2" />
|
||||
<line x1="16" y1="2" x2="16" y2="6" />
|
||||
<line x1="8" y1="2" x2="8" y2="6" />
|
||||
<line x1="3" y1="10" x2="21" y2="10" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<div class="truncate font-medium text-gray-900">{b.description || 'Untitled'}</div>
|
||||
<div class="flex flex-wrap items-center gap-x-2 gap-y-0.5 text-sm text-gray-600">
|
||||
<span>
|
||||
{new SvelteDate(b.start_time).toLocaleDateString('en-GB', {
|
||||
weekday: 'short',
|
||||
day: 'numeric',
|
||||
month: 'short'
|
||||
})}
|
||||
</span>
|
||||
<span class="text-gray-400">·</span>
|
||||
<span>
|
||||
{new SvelteDate(b.start_time).toLocaleTimeString('en-GB', {
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
hour12: true
|
||||
})}
|
||||
</span>
|
||||
<span class="text-gray-400">–</span>
|
||||
<span>
|
||||
{(() => {
|
||||
const end = new SvelteDate(b.start_time);
|
||||
end.setMinutes(end.getMinutes() + b.duration_minutes);
|
||||
return end.toLocaleTimeString('en-GB', {
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
hour12: true
|
||||
});
|
||||
})()}
|
||||
</span>
|
||||
<span class="rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium">
|
||||
{formatDuration(b.duration_minutes)}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-xs text-gray-400">
|
||||
{formatRelativeTime(b.start_time)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
class="shrink-0"
|
||||
onclick={() => {
|
||||
blockerToDelete = b;
|
||||
showDeleteAlert = true;
|
||||
}}
|
||||
>
|
||||
<div class="space-y-3">
|
||||
{#each pagedBlockers as b (b.id)}
|
||||
<div
|
||||
class="flex items-center justify-between gap-3 rounded-lg border p-3 transition-all hover:shadow-sm sm:p-4"
|
||||
>
|
||||
<div class="flex min-w-0 flex-1 items-center gap-3">
|
||||
<div class="hidden shrink-0 rounded-lg bg-gray-50 p-1.5 sm:block">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-3 w-3"
|
||||
class="h-4 w-4 text-gray-600"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<polyline points="3 6 5 6 21 6" />
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
|
||||
<rect x="3" y="4" width="18" height="18" rx="2" ry="2" />
|
||||
<line x1="16" y1="2" x2="16" y2="6" />
|
||||
<line x1="8" y1="2" x2="8" y2="6" />
|
||||
<line x1="3" y1="10" x2="21" y2="10" />
|
||||
</svg>
|
||||
</Button>
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<div class="truncate font-medium text-gray-900">{b.description || 'Untitled'}</div>
|
||||
<div class="flex flex-wrap items-center gap-x-2 gap-y-0.5 text-sm text-gray-600">
|
||||
<span>
|
||||
{new SvelteDate(b.start_time).toLocaleDateString('en-GB', {
|
||||
weekday: 'short',
|
||||
day: 'numeric',
|
||||
month: 'short'
|
||||
})}
|
||||
</span>
|
||||
<span class="text-gray-400">·</span>
|
||||
<span>
|
||||
{new SvelteDate(b.start_time).toLocaleTimeString('en-GB', {
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
hour12: true
|
||||
})}
|
||||
</span>
|
||||
<span class="text-gray-400">–</span>
|
||||
<span>
|
||||
{(() => {
|
||||
const end = new SvelteDate(b.start_time);
|
||||
end.setMinutes(end.getMinutes() + b.duration_minutes);
|
||||
return end.toLocaleTimeString('en-GB', {
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
hour12: true
|
||||
});
|
||||
})()}
|
||||
</span>
|
||||
<span class="rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium">
|
||||
{formatDuration(b.duration_minutes)}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-xs text-gray-400">
|
||||
{formatRelativeTime(b.start_time)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if totalPages > 1}
|
||||
<div class="flex flex-col-reverse gap-2 sm:flex-row sm:items-center sm:justify-between pt-2">
|
||||
<p class="text-sm text-gray-500">
|
||||
{(currentPage - 1) * PAGE_SIZE + 1}–{Math.min(currentPage * PAGE_SIZE, sortedBlockers.length)} of {sortedBlockers.length}
|
||||
</p>
|
||||
<div class="flex items-center gap-1">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={currentPage <= 1}
|
||||
onclick={() => (currentPage -= 1)}
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
class="shrink-0"
|
||||
onclick={() => {
|
||||
blockerToDelete = b;
|
||||
showDeleteAlert = true;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-3 w-3"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
Prev
|
||||
</Button>
|
||||
{#each Array(totalPages) as _, i (i)}
|
||||
<Button
|
||||
variant={currentPage === i + 1 ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
class="h-8 w-8 p-0"
|
||||
onclick={() => (currentPage = i + 1)}
|
||||
>
|
||||
{i + 1}
|
||||
</Button>
|
||||
{/each}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={currentPage >= totalPages}
|
||||
onclick={() => (currentPage += 1)}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
<polyline points="3 6 5 6 21 6" />
|
||||
<path
|
||||
d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"
|
||||
/>
|
||||
</svg>
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if totalPages > 1}
|
||||
<div
|
||||
class="flex flex-col-reverse gap-2 pt-2 sm:flex-row sm:items-center sm:justify-between"
|
||||
>
|
||||
<p class="text-sm text-gray-500">
|
||||
{(currentPage - 1) * PAGE_SIZE + 1}–{Math.min(
|
||||
currentPage * PAGE_SIZE,
|
||||
sortedBlockers.length
|
||||
)} of {sortedBlockers.length}
|
||||
</p>
|
||||
<div class="flex items-center gap-1">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={currentPage <= 1}
|
||||
onclick={() => (currentPage -= 1)}
|
||||
>
|
||||
Prev
|
||||
</Button>
|
||||
{#each Array(totalPages) as _, i (i)}
|
||||
<Button
|
||||
variant={currentPage === i + 1 ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
class="h-8 w-8 p-0"
|
||||
onclick={() => (currentPage = i + 1)}
|
||||
>
|
||||
{i + 1}
|
||||
</Button>
|
||||
{/each}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={currentPage >= totalPages}
|
||||
onclick={() => (currentPage += 1)}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</Card.Content>
|
||||
@@ -668,14 +702,37 @@
|
||||
<div class="space-y-2">
|
||||
<label for="blocker-start-date" class="text-xs text-gray-600">Date</label>
|
||||
<div class="flex gap-2">
|
||||
<Button type="button" variant="outline" size="sm" class="text-xs flex-1" onclick={() => { newStartDate = new Date().toISOString().slice(0, 10); onStartDateChange(); }}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="flex-1 text-xs"
|
||||
onclick={() => {
|
||||
newStartDate = new Date().toISOString().slice(0, 10);
|
||||
onStartDateChange();
|
||||
}}
|
||||
>
|
||||
Today
|
||||
</Button>
|
||||
<Button type="button" variant="outline" size="sm" class="text-xs flex-1" onclick={() => { newStartDate = new Date(Date.now() + 86400000).toISOString().slice(0, 10); onStartDateChange(); }}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="flex-1 text-xs"
|
||||
onclick={() => {
|
||||
newStartDate = new Date(Date.now() + 86400000).toISOString().slice(0, 10);
|
||||
onStartDateChange();
|
||||
}}
|
||||
>
|
||||
Tomorrow
|
||||
</Button>
|
||||
</div>
|
||||
<Input id="blocker-start-date" type="date" bind:value={newStartDate} onchange={onStartDateChange} />
|
||||
<Input
|
||||
id="blocker-start-date"
|
||||
type="date"
|
||||
bind:value={newStartDate}
|
||||
onchange={onStartDateChange}
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs text-gray-600">Time</label>
|
||||
@@ -684,7 +741,11 @@
|
||||
{:else if !selectedWorkingHours}
|
||||
<p class="text-sm text-gray-400">Select a date first</p>
|
||||
{:else if !selectedWorkingHours.isOpen}
|
||||
<p class="text-sm text-red-500">Closed on {new SvelteDate(newStartDate + 'T00:00:00').toLocaleDateString('en-GB', { weekday: 'long' })}</p>
|
||||
<p class="text-sm text-red-500">
|
||||
Closed on {new SvelteDate(newStartDate + 'T00:00:00').toLocaleDateString('en-GB', {
|
||||
weekday: 'long'
|
||||
})}
|
||||
</p>
|
||||
{:else}
|
||||
<select
|
||||
class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 text-sm"
|
||||
@@ -744,28 +805,51 @@
|
||||
{#if checkingOverlap}
|
||||
<div class="flex items-center gap-2 text-sm text-gray-500">
|
||||
<svg class="h-4 w-4 animate-spin" viewBox="0 0 24 24" fill="none">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||
<circle
|
||||
class="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
stroke-width="4"
|
||||
/>
|
||||
<path
|
||||
class="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
|
||||
/>
|
||||
</svg>
|
||||
Checking for conflicting bookings…
|
||||
</div>
|
||||
{:else if hasOverlap}
|
||||
<div class="rounded-lg border border-amber-200 bg-amber-50 p-4">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-amber-600" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
|
||||
<div class="mb-3 flex items-center gap-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4 text-amber-600"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
|
||||
/>
|
||||
<line x1="12" y1="9" x2="12" y2="13" />
|
||||
<line x1="12" y1="17" x2="12.01" y2="17" />
|
||||
</svg>
|
||||
<span class="text-sm font-medium text-amber-800">
|
||||
{overlappingBookings.length} booking{overlappingBookings.length > 1 ? 's' : ''} conflict{overlappingBookings.length > 1 ? '' : 's'} with this slot
|
||||
{overlappingBookings.length} booking{overlappingBookings.length > 1 ? 's' : ''} conflict{overlappingBookings.length >
|
||||
1
|
||||
? ''
|
||||
: 's'} with this slot
|
||||
</span>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
{#each overlappingBookings as booking (booking.id)}
|
||||
<div class="rounded-md border border-amber-200 bg-white p-3">
|
||||
<div class="min-w-0">
|
||||
<div class="font-medium text-sm">{booking.user?.full_name || 'Unknown'}</div>
|
||||
<div class="text-sm font-medium">{booking.user?.full_name || 'Unknown'}</div>
|
||||
<div class="text-xs text-gray-500">
|
||||
{new SvelteDate(booking.start_time).toLocaleTimeString('en-GB', {
|
||||
hour: 'numeric',
|
||||
@@ -780,12 +864,12 @@
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2 mt-2">
|
||||
<div class="mt-2 flex gap-2">
|
||||
{#if openBookingModal}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="text-xs h-7"
|
||||
class="h-7 text-xs"
|
||||
onclick={() => openBookingModal(booking.id)}
|
||||
>
|
||||
View Booking
|
||||
@@ -795,7 +879,7 @@
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="text-xs h-7"
|
||||
class="h-7 text-xs"
|
||||
onclick={() => openUserModal(booking.user!.id)}
|
||||
>
|
||||
View Client
|
||||
@@ -832,8 +916,8 @@
|
||||
<AlertDialog.Header>
|
||||
<AlertDialog.Title>Delete time blocker?</AlertDialog.Title>
|
||||
<AlertDialog.Description>
|
||||
This will remove the "{blockerToDelete?.description}" time blocker. Bookings may become available
|
||||
during this period. This action cannot be undone.
|
||||
This will remove the "{blockerToDelete?.description}" time blocker. Bookings may become
|
||||
available during this period. This action cannot be undone.
|
||||
</AlertDialog.Description>
|
||||
</AlertDialog.Header>
|
||||
<AlertDialog.Footer>
|
||||
|
||||
Reference in New Issue
Block a user