feat: add collapsible sections and lifted data fetching to admin page
Add independently toggleable collapsible sections for scheduling, customers, services, promotions, settings. Lift services and default-hours fetching to page level to avoid duplicate API calls. Add responsive breakpoint detection — sections default expanded on desktop, collapsed on mobile. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -19,6 +19,7 @@
|
|||||||
import BusinessSettings from '$lib/components/admin/BusinessSettings.svelte';
|
import BusinessSettings from '$lib/components/admin/BusinessSettings.svelte';
|
||||||
import UserModal from '$lib/components/admin/UserModal.svelte';
|
import UserModal from '$lib/components/admin/UserModal.svelte';
|
||||||
import BookingModal from '$lib/components/admin/BookingModal.svelte';
|
import BookingModal from '$lib/components/admin/BookingModal.svelte';
|
||||||
|
import ChevronDownIcon from '@lucide/svelte/icons/chevron-down';
|
||||||
|
|
||||||
// =============== Auth & Permissions ===============
|
// =============== Auth & Permissions ===============
|
||||||
let pageState = $state<'loading' | 'authorized' | 'unauthorized'>('loading');
|
let pageState = $state<'loading' | 'authorized' | 'unauthorized'>('loading');
|
||||||
@@ -56,6 +57,81 @@
|
|||||||
let selectedBookingId = $state<string | null>(null);
|
let selectedBookingId = $state<string | null>(null);
|
||||||
let rescheduleVersion = $state(0);
|
let rescheduleVersion = $state(0);
|
||||||
|
|
||||||
|
// =============== Collapsible Sections State ===============
|
||||||
|
// Each section is independently toggleable. State resets on page reload.
|
||||||
|
// Default expands all on desktop, collapses all on mobile.
|
||||||
|
let sectionState = $state({
|
||||||
|
scheduling: 'expanded',
|
||||||
|
customers: 'expanded',
|
||||||
|
services: 'expanded',
|
||||||
|
promotions: 'expanded',
|
||||||
|
settings: 'expanded'
|
||||||
|
});
|
||||||
|
|
||||||
|
let isMobile = $state(false);
|
||||||
|
|
||||||
|
// =============== Lifted Data Fetching ===============
|
||||||
|
// Fetch shared data once at page level to avoid duplicate API calls
|
||||||
|
let defaultHours = $state<Array<{ weekday: number; startTime: string; endTime: string; isOpen: boolean }>>([]);
|
||||||
|
let services = $state<Array<{ id: string; name: string; description: string; price: number; duration_minutes: number; is_active: boolean; patch_test_duration_hours: number; minimum_age_required: number; created_at: string; created_by?: string }>>([]);
|
||||||
|
|
||||||
|
// Responsive: reset section defaults when crossing the 768px breakpoint
|
||||||
|
$effect(() => {
|
||||||
|
if (!browser) return;
|
||||||
|
|
||||||
|
const mql = window.matchMedia('(max-width: 767px)');
|
||||||
|
isMobile = mql.matches;
|
||||||
|
|
||||||
|
function handleChange(e: MediaQueryListEvent) {
|
||||||
|
isMobile = e.matches;
|
||||||
|
for (const key in sectionState) {
|
||||||
|
sectionState[key as keyof typeof sectionState] = e.matches ? 'collapsed' : 'expanded';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mql.addEventListener('change', handleChange);
|
||||||
|
return () => mql.removeEventListener('change', handleChange);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fetch shared data once when authorized.
|
||||||
|
// Uses a plain (non-reactive) guard to prevent re-fetch loops.
|
||||||
|
// Svelte 5's $effect tracks reactive reads in the synchronous execution path,
|
||||||
|
// including inside async functions before the first `await`. Using a non-reactive
|
||||||
|
// `dataLoaded` flag prevents re-triggering when async callbacks complete.
|
||||||
|
let dataLoaded = false;
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (pageState !== 'authorized' || !browser || dataLoaded) return;
|
||||||
|
dataLoaded = true;
|
||||||
|
|
||||||
|
async function loadSharedData() {
|
||||||
|
try {
|
||||||
|
const [hoursRes, servicesRes] = await Promise.all([
|
||||||
|
fetch('/api/scheduling/default-hours', {
|
||||||
|
headers: { Authorization: `Bearer ${authStore.currentToken}` }
|
||||||
|
}),
|
||||||
|
fetch('/api/admin/services', {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${authStore.currentToken}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (hoursRes.ok) {
|
||||||
|
defaultHours = await hoursRes.json();
|
||||||
|
}
|
||||||
|
if (servicesRes.ok) {
|
||||||
|
services = await servicesRes.json();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error loading admin data:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadSharedData();
|
||||||
|
});
|
||||||
|
|
||||||
function openUserModal(userId: string) {
|
function openUserModal(userId: string) {
|
||||||
selectedUserId = userId;
|
selectedUserId = userId;
|
||||||
showUserModal = true;
|
showUserModal = true;
|
||||||
@@ -96,7 +172,6 @@
|
|||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
{#if pageState === 'loading'}
|
{#if pageState === 'loading'}
|
||||||
<!-- Full page skeleton loading -->
|
|
||||||
<div class="mx-auto max-w-6xl space-y-6 p-6">
|
<div class="mx-auto max-w-6xl space-y-6 p-6">
|
||||||
<!-- Header Skeleton -->
|
<!-- Header Skeleton -->
|
||||||
<div class="mb-8 flex items-center justify-center text-center">
|
<div class="mb-8 flex items-center justify-center text-center">
|
||||||
@@ -106,189 +181,76 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Image Upload Card Skeleton -->
|
<!-- 1. Portfolio Images -->
|
||||||
<div class="rounded-lg border p-6">
|
<div class="rounded-lg border p-6">
|
||||||
<div class="mb-4 space-y-2">
|
<div class="space-y-2">
|
||||||
<Skeleton class="h-6 w-32" />
|
|
||||||
<Skeleton class="h-4 w-48" />
|
|
||||||
</div>
|
|
||||||
<Skeleton class="h-32 w-full" />
|
|
||||||
<div class="mt-4 flex justify-end">
|
|
||||||
<Skeleton class="h-10 w-40" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Users & Bookings Grid Skeleton -->
|
|
||||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
||||||
<!-- Users Card Skeleton -->
|
|
||||||
<div class="rounded-lg border p-6">
|
|
||||||
<div class="mb-4 space-y-2">
|
|
||||||
<Skeleton class="h-6 w-20" />
|
|
||||||
<Skeleton class="h-4 w-40" />
|
|
||||||
</div>
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<Skeleton class="h-10 flex-1" />
|
|
||||||
<Skeleton class="h-10 w-20" />
|
|
||||||
</div>
|
|
||||||
<div class="mt-4 space-y-2">
|
|
||||||
{#each Array(3) as _, i (i)}
|
|
||||||
<Skeleton class="h-16 w-full" />
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Bookings Card Skeleton -->
|
|
||||||
<div class="rounded-lg border p-6">
|
|
||||||
<div class="mb-4 space-y-2">
|
|
||||||
<Skeleton class="h-6 w-24" />
|
|
||||||
<Skeleton class="h-4 w-40" />
|
|
||||||
</div>
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<Skeleton class="h-10 flex-1" />
|
|
||||||
<Skeleton class="h-10 w-20" />
|
|
||||||
</div>
|
|
||||||
<div class="mt-4 space-y-2">
|
|
||||||
{#each Array(3) as _, i (i)}
|
|
||||||
<Skeleton class="h-16 w-full" />
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Time Blockers Card Skeleton -->
|
|
||||||
<div class="rounded-lg border p-6">
|
|
||||||
<div class="mb-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
||||||
<div class="space-y-2">
|
|
||||||
<Skeleton class="h-6 w-32" />
|
|
||||||
<Skeleton class="h-4 w-80" />
|
|
||||||
</div>
|
|
||||||
<Skeleton class="h-10 w-28" />
|
|
||||||
</div>
|
|
||||||
<div class="mt-4 space-y-3">
|
|
||||||
{#each Array(3) as _, i (i)}
|
|
||||||
<Skeleton class="h-16 w-full" />
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Holiday Hours Card Skeleton -->
|
|
||||||
|
|
||||||
<!-- Working Hours Card Skeleton -->
|
|
||||||
<div class="rounded-lg border p-6">
|
|
||||||
<div class="mb-4 space-y-2">
|
|
||||||
<Skeleton class="h-6 w-32" />
|
|
||||||
<Skeleton class="h-4 w-64" />
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between">
|
|
||||||
<Skeleton class="h-6 w-40" />
|
<Skeleton class="h-6 w-40" />
|
||||||
<Skeleton class="h-10 w-24" />
|
<Skeleton class="h-4 w-56" />
|
||||||
</div>
|
|
||||||
<div class="mt-4 w-full overflow-x-auto">
|
|
||||||
<table class="w-full table-auto">
|
|
||||||
<thead>
|
|
||||||
<tr class="text-left text-xs text-gray-500">
|
|
||||||
<th class="py-2"><Skeleton class="h-4 w-12" /></th>
|
|
||||||
<th class="py-2"><Skeleton class="h-4 w-16" /></th>
|
|
||||||
<th class="py-2"><Skeleton class="h-4 w-16" /></th>
|
|
||||||
<th class="py-2"><Skeleton class="h-4 w-16" /></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{#each Array(7) as _, i (i)}
|
|
||||||
<tr class="border-t">
|
|
||||||
<td class="py-2"><Skeleton class="h-4 w-20" /></td>
|
|
||||||
<td class="py-2"><Skeleton class="h-4 w-12" /></td>
|
|
||||||
<td class="py-2"><Skeleton class="h-4 w-12" /></td>
|
|
||||||
<td class="py-2"><Skeleton class="h-4 w-12" /></td>
|
|
||||||
</tr>
|
|
||||||
{/each}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
|
<Skeleton class="mt-4 h-32 w-full rounded-lg" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Services Management Card Skeleton -->
|
<!-- 2. Customers & Bookings -->
|
||||||
<div class="rounded-lg border p-6">
|
<div class="rounded-lg border p-6">
|
||||||
<div class="mb-4 space-y-2">
|
<div class="flex items-center justify-between">
|
||||||
<Skeleton class="h-6 w-40" />
|
|
||||||
<Skeleton class="h-4 w-80" />
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between">
|
|
||||||
<Skeleton class="h-10 w-32" />
|
|
||||||
</div>
|
|
||||||
<div class="mt-4 hidden w-full overflow-x-auto md:block">
|
|
||||||
<table class="w-full table-auto border-collapse text-sm">
|
|
||||||
<thead>
|
|
||||||
<tr class="border-b text-left text-xs text-gray-500">
|
|
||||||
<th class="py-3"><Skeleton class="h-4 w-20" /></th>
|
|
||||||
<th class="py-3"><Skeleton class="h-4 w-32" /></th>
|
|
||||||
<th class="py-3"><Skeleton class="h-4 w-16" /></th>
|
|
||||||
<th class="py-3"><Skeleton class="h-4 w-20" /></th>
|
|
||||||
<th class="py-3"><Skeleton class="h-4 w-16" /></th>
|
|
||||||
<th class="py-3"><Skeleton class="h-4 w-24" /></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{#each Array(3) as _, i (i)}
|
|
||||||
<tr class="border-b">
|
|
||||||
<td class="py-3"><Skeleton class="h-4 w-32" /></td>
|
|
||||||
<td class="py-3"><Skeleton class="h-4 w-48" /></td>
|
|
||||||
<td class="py-3"><Skeleton class="h-4 w-16" /></td>
|
|
||||||
<td class="py-3"><Skeleton class="h-4 w-20" /></td>
|
|
||||||
<td class="py-3"><Skeleton class="h-4 w-16" /></td>
|
|
||||||
<td class="py-3">
|
|
||||||
<div class="flex justify-center gap-2">
|
|
||||||
<Skeleton class="h-8 w-16" />
|
|
||||||
<Skeleton class="h-8 w-16" />
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{/each}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Discounts Management Card Skeleton -->
|
|
||||||
<div class="rounded-lg border p-6">
|
|
||||||
<div class="mb-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Skeleton class="h-6 w-40" />
|
<Skeleton class="h-6 w-48" />
|
||||||
|
<Skeleton class="h-4 w-72" />
|
||||||
|
</div>
|
||||||
|
<Skeleton class="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
|
<Skeleton class="h-40 w-full rounded-lg" />
|
||||||
|
<Skeleton class="h-40 w-full rounded-lg" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 3. Services & Products -->
|
||||||
|
<div class="rounded-lg border p-6">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Skeleton class="h-6 w-48" />
|
||||||
<Skeleton class="h-4 w-64" />
|
<Skeleton class="h-4 w-64" />
|
||||||
</div>
|
</div>
|
||||||
<Skeleton class="h-10 w-32" />
|
<Skeleton class="h-5 w-5" />
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-4 hidden w-full overflow-x-auto md:block">
|
<Skeleton class="mt-4 h-24 w-full" />
|
||||||
<table class="w-full table-auto border-collapse text-sm">
|
</div>
|
||||||
<thead>
|
|
||||||
<tr class="border-b text-left text-xs text-gray-500">
|
<!-- 4. Scheduling & Hours -->
|
||||||
<th class="py-3"><Skeleton class="h-4 w-24" /></th>
|
<div class="rounded-lg border p-6">
|
||||||
<th class="py-3"><Skeleton class="h-4 w-20" /></th>
|
<div class="flex items-center justify-between">
|
||||||
<th class="py-3"><Skeleton class="h-4 w-16" /></th>
|
<div class="space-y-2">
|
||||||
<th class="py-3"><Skeleton class="h-4 w-16" /></th>
|
<Skeleton class="h-6 w-44" />
|
||||||
<th class="py-3"><Skeleton class="h-4 w-16" /></th>
|
<Skeleton class="h-4 w-64" />
|
||||||
<th class="py-3"><Skeleton class="h-4 w-40" /></th>
|
</div>
|
||||||
</tr>
|
<Skeleton class="h-5 w-5" />
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{#each Array(2) as _, i (i)}
|
|
||||||
<tr class="border-b">
|
|
||||||
<td class="py-3"><Skeleton class="h-4 w-32" /></td>
|
|
||||||
<td class="py-3"><Skeleton class="h-4 w-24" /></td>
|
|
||||||
<td class="py-3"><Skeleton class="h-4 w-12" /></td>
|
|
||||||
<td class="py-3"><Skeleton class="h-4 w-16" /></td>
|
|
||||||
<td class="py-3"><Skeleton class="h-4 w-12" /></td>
|
|
||||||
<td class="py-3">
|
|
||||||
<div class="flex justify-end gap-2">
|
|
||||||
<Skeleton class="h-8 w-16" />
|
|
||||||
<Skeleton class="h-8 w-16" />
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{/each}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
|
<Skeleton class="mt-4 h-24 w-full" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 5. Promotions & Finance -->
|
||||||
|
<div class="rounded-lg border p-6">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Skeleton class="h-6 w-48" />
|
||||||
|
<Skeleton class="h-4 w-64" />
|
||||||
|
</div>
|
||||||
|
<Skeleton class="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
<Skeleton class="mt-4 h-24 w-full" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 6. Business Settings -->
|
||||||
|
<div class="rounded-lg border p-6">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Skeleton class="h-6 w-44" />
|
||||||
|
<Skeleton class="h-4 w-56" />
|
||||||
|
</div>
|
||||||
|
<Skeleton class="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
<Skeleton class="mt-4 h-24 w-full" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else if pageState === 'authorized'}
|
{:else if pageState === 'authorized'}
|
||||||
@@ -300,20 +262,131 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 1. Portfolio Images -->
|
||||||
<ImageUpload />
|
<ImageUpload />
|
||||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
||||||
<UsersCard {openUserModal} />
|
<!-- 2. Customers & Bookings -->
|
||||||
<BookingsCard {openBookingModal} />
|
<div class="rounded-lg border">
|
||||||
|
<button
|
||||||
|
class="flex w-full items-center justify-between bg-gray-50 px-4 py-3 transition-colors hover:bg-gray-100 {sectionState.customers === 'expanded' ? 'rounded-t-lg' : 'rounded-lg'}"
|
||||||
|
style="min-height: 44px;"
|
||||||
|
onclick={() => sectionState.customers = sectionState.customers === 'expanded' ? 'collapsed' : 'expanded'}
|
||||||
|
aria-expanded={sectionState.customers === 'expanded'}
|
||||||
|
>
|
||||||
|
<span class="font-medium text-gray-900">Customers & Bookings</span>
|
||||||
|
<ChevronDownIcon
|
||||||
|
class="h-4 w-4 text-gray-500 transition-transform duration-200 {sectionState.customers === 'expanded' ? '' : '-rotate-90'}"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
{#if sectionState.customers === 'expanded'}
|
||||||
|
<div class="p-4">
|
||||||
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
|
<UsersCard {openUserModal} />
|
||||||
|
<BookingsCard {openBookingModal} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 3. Services & Products -->
|
||||||
|
<div class="rounded-lg border">
|
||||||
|
<button
|
||||||
|
class="flex w-full items-center justify-between bg-gray-50 px-4 py-3 transition-colors hover:bg-gray-100 {sectionState.services === 'expanded' ? 'rounded-t-lg' : 'rounded-lg'}"
|
||||||
|
style="min-height: 44px;"
|
||||||
|
onclick={() => sectionState.services = sectionState.services === 'expanded' ? 'collapsed' : 'expanded'}
|
||||||
|
aria-expanded={sectionState.services === 'expanded'}
|
||||||
|
>
|
||||||
|
<span class="font-medium text-gray-900">Services & Products</span>
|
||||||
|
<ChevronDownIcon
|
||||||
|
class="h-4 w-4 text-gray-500 transition-transform duration-200 {sectionState.services === 'expanded' ? '' : '-rotate-90'}"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
{#if sectionState.services === 'expanded'}
|
||||||
|
<div class="space-y-4 p-4">
|
||||||
|
<CustomServicesManagement />
|
||||||
|
<PatchTestsManagement services={services} />
|
||||||
|
<ServicesManagement services={services} onRefresh={async () => {
|
||||||
|
if (!browser) return;
|
||||||
|
try {
|
||||||
|
const r = await fetch('/api/admin/services', {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${authStore.currentToken}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (r.ok) {
|
||||||
|
services = await r.json();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error refreshing services:', err);
|
||||||
|
}
|
||||||
|
}} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 4. Scheduling & Hours -->
|
||||||
|
<div class="rounded-lg border">
|
||||||
|
<button
|
||||||
|
class="flex w-full items-center justify-between bg-gray-50 px-4 py-3 transition-colors hover:bg-gray-100 {sectionState.scheduling === 'expanded' ? 'rounded-t-lg' : 'rounded-lg'}"
|
||||||
|
style="min-height: 44px;"
|
||||||
|
onclick={() => sectionState.scheduling = sectionState.scheduling === 'expanded' ? 'collapsed' : 'expanded'}
|
||||||
|
aria-expanded={sectionState.scheduling === 'expanded'}
|
||||||
|
>
|
||||||
|
<span class="font-medium text-gray-900">Scheduling & Hours</span>
|
||||||
|
<ChevronDownIcon
|
||||||
|
class="h-4 w-4 text-gray-500 transition-transform duration-200 {sectionState.scheduling === 'expanded' ? '' : '-rotate-90'}"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
{#if sectionState.scheduling === 'expanded'}
|
||||||
|
<div class="space-y-4 p-4">
|
||||||
|
<TimeBlockers {openUserModal} {openBookingModal} {rescheduleVersion} defaultHours={defaultHours} />
|
||||||
|
<HolidayHours />
|
||||||
|
<WeeklySchedule defaultHours={defaultHours} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 5. Promotions & Finance -->
|
||||||
|
<div class="rounded-lg border">
|
||||||
|
<button
|
||||||
|
class="flex w-full items-center justify-between bg-gray-50 px-4 py-3 transition-colors hover:bg-gray-100 {sectionState.promotions === 'expanded' ? 'rounded-t-lg' : 'rounded-lg'}"
|
||||||
|
style="min-height: 44px;"
|
||||||
|
onclick={() => sectionState.promotions = sectionState.promotions === 'expanded' ? 'collapsed' : 'expanded'}
|
||||||
|
aria-expanded={sectionState.promotions === 'expanded'}
|
||||||
|
>
|
||||||
|
<span class="font-medium text-gray-900">Promotions & Finance</span>
|
||||||
|
<ChevronDownIcon
|
||||||
|
class="h-4 w-4 text-gray-500 transition-transform duration-200 {sectionState.promotions === 'expanded' ? '' : '-rotate-90'}"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
{#if sectionState.promotions === 'expanded'}
|
||||||
|
<div class="space-y-4 p-4">
|
||||||
|
<DiscountsManagement />
|
||||||
|
<GiftCardsManagement />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 6. Business Settings -->
|
||||||
|
<div class="rounded-lg border">
|
||||||
|
<button
|
||||||
|
class="flex w-full items-center justify-between bg-gray-50 px-4 py-3 transition-colors hover:bg-gray-100 {sectionState.settings === 'expanded' ? 'rounded-t-lg' : 'rounded-lg'}"
|
||||||
|
style="min-height: 44px;"
|
||||||
|
onclick={() => sectionState.settings = sectionState.settings === 'expanded' ? 'collapsed' : 'expanded'}
|
||||||
|
aria-expanded={sectionState.settings === 'expanded'}
|
||||||
|
>
|
||||||
|
<span class="font-medium text-gray-900">Business Settings</span>
|
||||||
|
<ChevronDownIcon
|
||||||
|
class="h-4 w-4 text-gray-500 transition-transform duration-200 {sectionState.settings === 'expanded' ? '' : '-rotate-90'}"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
{#if sectionState.settings === 'expanded'}
|
||||||
|
<div class="p-4">
|
||||||
|
<BusinessSettings />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<TimeBlockers {openUserModal} {openBookingModal} {rescheduleVersion} />
|
|
||||||
<HolidayHours />
|
|
||||||
<WeeklySchedule />
|
|
||||||
<ServicesManagement />
|
|
||||||
<CustomServicesManagement />
|
|
||||||
<PatchTestsManagement />
|
|
||||||
<DiscountsManagement />
|
|
||||||
<GiftCardsManagement />
|
|
||||||
<BusinessSettings />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Modals -->
|
<!-- Modals -->
|
||||||
|
|||||||
Reference in New Issue
Block a user