refactor: admin management components (discounts, holidays, schedule, time blockers)

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 16:07:44 +01:00
co-authored by Sisyphus
parent 96ab71a579
commit b2788b2269
4 changed files with 18 additions and 21 deletions
@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { SvelteDate } from 'svelte/reactivity';
import { authStore } from '$lib/stores/auth.svelte'; import { authStore } from '$lib/stores/auth.svelte';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
@@ -95,7 +96,7 @@
if (form.discount_percent <= 0 || form.discount_percent > 100) return false; if (form.discount_percent <= 0 || form.discount_percent > 100) return false;
if (form.campaign_type === 'time_based') { if (form.campaign_type === 'time_based') {
if (!form.start_date || !form.end_date) return false; if (!form.start_date || !form.end_date) return false;
if (new Date(form.end_date) <= new Date(form.start_date)) return false; if (new SvelteDate(form.end_date) <= new SvelteDate(form.start_date)) return false;
} }
if (form.campaign_type === 'milestone') { if (form.campaign_type === 'milestone') {
if (form.milestone_value <= 0) return false; if (form.milestone_value <= 0) return false;
@@ -535,7 +535,7 @@
class="w-24 text-sm" class="w-24 text-sm"
> >
{#each Array(24) as _, hour} {#each Array(24) as _, hour}
{#each TIME15 as min} {#each TIME15 as min (min)}
<option value="{String(hour).padStart(2,'0')}:{min}">{String(hour).padStart(2,'0')}:{min}</option> <option value="{String(hour).padStart(2,'0')}:{min}">{String(hour).padStart(2,'0')}:{min}</option>
{/each} {/each}
{/each} {/each}
@@ -548,7 +548,7 @@
class="w-24 text-sm" class="w-24 text-sm"
> >
{#each Array(24) as _, hour} {#each Array(24) as _, hour}
{#each TIME15 as min} {#each TIME15 as min (min)}
<option value="{String(hour).padStart(2,'0')}:{min}">{String(hour).padStart(2,'0')}:{min}</option> <option value="{String(hour).padStart(2,'0')}:{min}">{String(hour).padStart(2,'0')}:{min}</option>
{/each} {/each}
{/each} {/each}
@@ -2,6 +2,7 @@
import { authStore } from '$lib/stores/auth.svelte'; import { authStore } from '$lib/stores/auth.svelte';
import { SvelteDate } from 'svelte/reactivity'; import { SvelteDate } from 'svelte/reactivity';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { formatDuration } from '$lib/utils/format';
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import * as Card from '$lib/components/ui/card'; import * as Card from '$lib/components/ui/card';
@@ -110,7 +111,7 @@
function getWorkingHoursForDate(dateStr: string): WorkingHourRow | null { function getWorkingHoursForDate(dateStr: string): WorkingHourRow | null {
if (!dateStr || defaultHours.length === 0) return null; if (!dateStr || defaultHours.length === 0) return null;
const d = new Date(dateStr + 'T00:00:00'); const d = new SvelteDate(dateStr + 'T00:00:00');
const jsDay = d.getDay(); const jsDay = d.getDay();
const weekday = jsDay === 0 ? 6 : jsDay - 1; const weekday = jsDay === 0 ? 6 : jsDay - 1;
return defaultHours.find((h) => h.weekday === weekday) ?? null; return defaultHours.find((h) => h.weekday === weekday) ?? null;
@@ -152,16 +153,11 @@
return options; return options;
}); });
function formatDuration(minutes: number): string {
if (minutes < 60) return `${minutes}m`;
const h = Math.floor(minutes / 60);
const m = minutes % 60;
return m > 0 ? `${h}h ${m}m` : `${h}h`;
}
function formatRelativeTime(iso: string): string { function formatRelativeTime(iso: string): string {
const d = new Date(iso); const d = new SvelteDate(iso);
const now = new Date(); const now = new SvelteDate();
const diffMs = d.getTime() - now.getTime(); const diffMs = d.getTime() - now.getTime();
if (diffMs < 0) { if (diffMs < 0) {
const absMin = Math.floor(Math.abs(diffMs) / 60000); const absMin = Math.floor(Math.abs(diffMs) / 60000);
@@ -178,7 +174,7 @@
} }
let sortedBlockers = $derived.by(() => { let sortedBlockers = $derived.by(() => {
return [...blockers].sort((a, b) => new Date(a.start_time).getTime() - new Date(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))); let totalPages = $derived.by(() => Math.max(1, Math.ceil(sortedBlockers.length / PAGE_SIZE)));
@@ -261,8 +257,8 @@
return; return;
} }
const blockerStart = new Date(startIso); const blockerStart = new SvelteDate(startIso);
const blockerEnd = new Date(endIso); const blockerEnd = new SvelteDate(endIso);
if (blockerEnd.getTime() <= blockerStart.getTime()) { if (blockerEnd.getTime() <= blockerStart.getTime()) {
overlappingBookings = []; overlappingBookings = [];
hasOverlap = false; hasOverlap = false;
@@ -288,8 +284,8 @@
const allBookings: OverlappingBooking[] = data.bookings || []; const allBookings: OverlappingBooking[] = data.bookings || [];
// Client-side filter: only bookings that overlap with the proposed blocker timespan // Client-side filter: only bookings that overlap with the proposed blocker timespan
const filtered = allBookings.filter((b) => { const filtered = allBookings.filter((b) => {
const bStart = new Date(b.start_time); const bStart = new SvelteDate(b.start_time);
const bEnd = new Date(bStart.getTime() + b.duration_minutes * 60000); const bEnd = new SvelteDate(bStart.getTime() + b.duration_minutes * 60000);
return bStart < blockerEnd && bEnd > blockerStart; return bStart < blockerEnd && bEnd > blockerStart;
}); });
overlappingBookings = filtered; overlappingBookings = filtered;
@@ -701,7 +697,7 @@
onStartTimeChange(); onStartTimeChange();
}} }}
> >
{#each availableStartOptions as opt} {#each availableStartOptions as opt (opt.totalMin)}
<option value={`${opt.hour}:${opt.minute}:${opt.period}`}> <option value={`${opt.hour}:${opt.minute}:${opt.period}`}>
{opt.label} {opt.label}
</option> </option>
@@ -736,7 +732,7 @@
onEndTimeChange(); onEndTimeChange();
}} }}
> >
{#each availableEndOptions as opt} {#each availableEndOptions as opt (opt.totalMin)}
<option value={`${opt.hour}:${opt.minute}:${opt.period}`}> <option value={`${opt.hour}:${opt.minute}:${opt.period}`}>
{opt.label} {opt.label}
</option> </option>
@@ -486,7 +486,7 @@
class="max-w-[70px] text-sm" class="max-w-[70px] text-sm"
> >
{#each Array(24) as _, hour} {#each Array(24) as _, hour}
{#each TIME15 as min} {#each TIME15 as min (min)}
<option value="{String(hour).padStart(2,'0')}:{min}">{String(hour).padStart(2,'0')}:{min}</option> <option value="{String(hour).padStart(2,'0')}:{min}">{String(hour).padStart(2,'0')}:{min}</option>
{/each} {/each}
{/each} {/each}
@@ -499,7 +499,7 @@
class="max-w-[70px] text-sm" class="max-w-[70px] text-sm"
> >
{#each Array(24) as _, hour} {#each Array(24) as _, hour}
{#each TIME15 as min} {#each TIME15 as min (min)}
<option value="{String(hour).padStart(2,'0')}:{min}">{String(hour).padStart(2,'0')}:{min}</option> <option value="{String(hour).padStart(2,'0')}:{min}">{String(hour).padStart(2,'0')}:{min}</option>
{/each} {/each}
{/each} {/each}