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:
2026-05-29 20:23:14 +01:00
co-authored by Sisyphus
parent ef650e013a
commit 6e901690fd
131 changed files with 2437 additions and 1571 deletions
@@ -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>