Fix admin

This commit is contained in:
2025-10-22 17:50:17 +01:00
parent a141404f74
commit 94404c7b3b
2 changed files with 432 additions and 74 deletions
+429 -71
View File
@@ -961,6 +961,28 @@
showServiceModal = true;
}
/** Calculate hours between start and end time */
function calculateHours(startTime: string, endTime: string): string {
// Convert formatted times to 24-hour format for calculation
const start = timeToInputValue(startTime);
const end = timeToInputValue(endTime);
const [startHours, startMinutes] = start.split(':').map(Number);
const [endHours, endMinutes] = end.split(':').map(Number);
const startTotalMinutes = startHours * 60 + startMinutes;
const endTotalMinutes = endHours * 60 + endMinutes;
const diffMinutes = endTotalMinutes - startTotalMinutes;
const hours = Math.floor(diffMinutes / 60);
const minutes = diffMinutes % 60;
if (minutes === 0) {
return `${hours}`;
}
return `${hours}.${minutes === 30 ? '5' : Math.round((minutes / 60) * 10)}`;
}
// Fetch services on component mount
$effect(() => {
if (pageState === 'authorized') {
@@ -1151,8 +1173,26 @@
<Card.Root>
<Card.Header>
<Card.Title>Image Upload</Card.Title>
<Card.Description>Upload images for the portfolio or other uses.</Card.Description>
<div class="flex items-center justify-between">
<div>
<Card.Title>Image Upload</Card.Title>
<Card.Description>Upload images for the portfolio or other uses.</Card.Description>
</div>
<div class="hidden rounded-lg p-2 md:block">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
<circle cx="8.5" cy="8.5" r="1.5" />
<polyline points="21 15 16 10 5 21" />
</svg>
</div>
</div>
</Card.Header>
<Card.Content class="space-y-4">
<FileDropZone onfiles={handleFilesDropped} accept="image/*" multiple>
@@ -1205,8 +1245,31 @@
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<Card.Root class="h-full">
<Card.Header>
<Card.Title>Users</Card.Title>
<Card.Description>Search and manage user details.</Card.Description>
<div class="flex items-start justify-between">
<div>
<Card.Title class="flex items-center gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
<circle cx="9" cy="7" r="4" />
<path d="M23 21v-2a4 4 0 0 0-3-3.87" />
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
</svg>
Users
</Card.Title>
<Card.Description>Search and manage user details.</Card.Description>
</div>
<div class="rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium">
<span class="text-xs font-semibold">{users.length}</span>
</div>
</div>
</Card.Header>
<Card.Content class="space-y-4">
<div>
@@ -1238,8 +1301,30 @@
<Card.Root class="h-full">
<Card.Header>
<Card.Title>Bookings</Card.Title>
<Card.Description>Search and manage booking history.</Card.Description>
<div class="flex items-start justify-between">
<div>
<Card.Title class="flex items-center gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
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>
Bookings
</Card.Title>
<Card.Description>Search and manage booking history.</Card.Description>
</div>
<div class="rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium">
<span class="text-xs font-semibold">{bookings.length}</span>
</div>
</div>
</Card.Header>
<Card.Content class="space-y-4">
<div>
@@ -1274,17 +1359,31 @@
<Card.Root>
<Card.Header>
<Card.Title>Holiday Hours</Card.Title>
<Card.Description>
Manage temporary schedules for holidays, closures, and special events.
</Card.Description>
<div class="flex items-center justify-between">
<div>
<Card.Title>Holiday Hours</Card.Title>
<Card.Description>
Manage temporary schedules for holidays, closures, and special events.
</Card.Description>
</div>
<Button variant="default" onclick={createNewException}>
<svg
xmlns="http://www.w3.org/2000/svg"
class="mr-2 h-4 w-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<line x1="12" y1="5" x2="12" y2="19" />
<line x1="5" y1="12" x2="19" y2="12" />
</svg>
New Schedule
</Button>
</div>
</Card.Header>
<Card.Content class="space-y-4">
<div class="flex items-center justify-between">
<Button variant="default" onclick={createNewException}>New schedule</Button>
</div>
{#if exceptionGroupsLoading}
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
{#each Array(2) as _, i}
@@ -1298,22 +1397,75 @@
{/if}
{#each exceptionGroups as g}
<div class="relative h-full rounded border p-3 pb-12">
<div class="group relative h-full rounded-lg border p-4 transition-all">
<div class="flex h-full flex-col gap-3">
<div class="flex-1">
<div class="font-semibold">{g.name}</div>
<div class="text-sm text-gray-600">{g.description}</div>
<div class="mt-1 text-xs text-gray-500">
Applies to weeks: {g.weekStarts?.slice(0, 5).join(', ')}
{#if (g.weekStarts?.length ?? 0) > 5}
(+{(g.weekStarts?.length ?? 0) - 5} more)
{/if}
<div class="mb-2 flex items-start justify-between">
<div class="flex items-center gap-2">
<div class="rounded-lg bg-gray-50 p-2">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
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>
<h3 class="font-semibold text-gray-900">{g.name}</h3>
</div>
<span class="rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium">
{g.weekStarts?.length || 0} weeks
</span>
</div>
<p class="mb-3 text-sm text-gray-600">{g.description}</p>
<div class="rounded-lg bg-gray-50 p-2">
<div class="mb-1 text-xs font-medium text-gray-500">Applies to weeks:</div>
<div class="text-xs text-gray-700">
{g.weekStarts
?.slice(0, 3)
.map((w) =>
new Date(w).toLocaleDateString('en-GB', {
day: 'numeric',
month: 'short'
})
)
.join(', ')}
{#if (g.weekStarts?.length ?? 0) > 3}
<span class="text-gray-500">
(+{(g.weekStarts?.length ?? 0) - 3} more)</span
>
{/if}
</div>
</div>
</div>
<div class="absolute bottom-3 right-3 flex gap-2">
<Button variant="default" size="sm" onclick={() => openViewExceptionModal(g)}>
View
<div class="flex gap-2 border-t pt-2">
<Button
variant="outline"
size="sm"
onclick={() => openViewExceptionModal(g)}
class="flex-1"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="mr-1 h-3 w-3"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
<circle cx="12" cy="12" r="3" />
</svg>
View Details
</Button>
<Button
variant="destructive"
@@ -1323,7 +1475,19 @@
showDeleteExceptionAlert = true;
}}
>
Delete
<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"
>
<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>
</div>
@@ -1336,94 +1500,288 @@
<!-- Default Hours Card -->
<Card.Root>
<Card.Header>
<Card.Title>Working Hours</Card.Title>
<Card.Description>View and manage your standard weekly schedule.</Card.Description>
</Card.Header>
{#if !defaultHoursIsLoading}
<Card.Content class="space-y-4">
<div class="flex items-center justify-between">
<h3 class="font-semibold">Weekly Schedule</h3>
<div>
<h3 class="text-lg font-semibold">Weekly Schedule</h3>
<p class="text-sm text-gray-500">
Your standard operating hours for each day of the week
</p>
</div>
<Button onclick={prepareDefaultHoursEdit} disabled={loadingHours || savingHours}>
Edit Defaults
<svg
xmlns="http://www.w3.org/2000/svg"
class="mr-2 h-4 w-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
</svg>
Edit Schedule
</Button>
</div>
<div class="w-full overflow-x-auto">
<table class="w-full table-auto">
<!-- Desktop Table -->
<div class="hidden w-full overflow-x-auto sm:block">
<table class="w-full table-auto border-collapse">
<thead>
<tr class="text-left text-xs text-gray-500">
<th class="py-2">Day</th>
<th class="py-2">Status</th>
<th class="py-2">Start</th>
<th class="py-2">End</th>
<tr class="border-b text-left text-xs text-gray-500">
<th class="px-4 py-3">Day</th>
<th class="px-4 py-3 text-center">Status</th>
<th class="px-4 py-3">Opening Time</th>
<th class="px-4 py-3">Closing Time</th>
<th class="px-4 py-3 text-right">Total Hours</th>
</tr>
</thead>
<tbody>
<tbody class="divide-y divide-gray-200">
{#each defaultHours as row}
<tr class="border-t">
<td class="py-2 text-sm">{weekdayLabel(row.weekday)}</td>
<td class="py-2">
<tr class="transition-colors hover:bg-gray-50">
<td class="p-3">
<div class="flex items-center gap-2">
<span class="font-medium"
>{weekdayLabel(row.weekday) === 'Mon'
? 'Monday'
: weekdayLabel(row.weekday) === 'Tue'
? 'Tuesday'
: weekdayLabel(row.weekday) === 'Wed'
? 'Wednesday'
: weekdayLabel(row.weekday) === 'Thu'
? 'Thursday'
: weekdayLabel(row.weekday) === 'Fri'
? 'Friday'
: weekdayLabel(row.weekday) === 'Sat'
? 'Saturday'
: 'Sunday'}</span
>
</div>
</td>
<td class="px-4 py-4 text-center">
<span
class="text-sm font-medium {row.is_open
? 'text-emerald-600'
: 'text-red-600'}"
class="inline-flex items-center rounded-full px-3 py-1 text-xs font-medium {row.is_open
? 'bg-emerald-100 text-emerald-800'
: 'bg-gray-100 text-gray-800'}"
>
<span
class="mr-1.5 h-1.5 w-1.5 rounded-full {row.is_open
? 'bg-emerald-600'
: 'bg-gray-600'}"
></span>
{row.is_open ? 'Open' : 'Closed'}
</span>
</td>
<td class="py-2 text-sm">
{row.is_open ? row.start_time : '—'}
<td class="px-4 py-4">
{#if row.is_open}
<div class="flex items-center gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4 text-gray-400"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="12" cy="12" r="10" />
<polyline points="12 6 12 12 16 14" />
</svg>
<span class="font-medium text-gray-900">{row.start_time}</span>
</div>
{:else}
<span class="text-gray-400">—</span>
{/if}
</td>
<td class="py-2 text-sm">
{row.is_open ? row.end_time : '—'}
<td class="px-4 py-4">
{#if row.is_open}
<div class="flex items-center gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4 text-gray-400"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="12" cy="12" r="10" />
<polyline points="12 6 12 12 16 14" />
</svg>
<span class="font-medium text-gray-900">{row.end_time}</span>
</div>
{:else}
<span class="text-gray-400">—</span>
{/if}
</td>
<td class="px-4 py-4 text-right">
{#if row.is_open}
<span
class="inline-flex items-center gap-1 text-sm font-medium text-gray-700"
>
{calculateHours(row.start_time, row.end_time)}
<span class="text-xs text-gray-500">hrs</span>
</span>
{:else}
<span class="text-gray-400">—</span>
{/if}
</td>
</tr>
{/each}
</tbody>
</table>
</div>
<!-- Mobile Cards -->
<div class="space-y-3 sm:hidden">
{#each defaultHours as row}
<div class="rounded-lg border p-4 transition-colors hover:bg-gray-50">
<div class="mb-3 flex items-center justify-between">
<div class="flex items-center gap-2">
<span class="font-semibold text-gray-900">
{weekdayLabel(row.weekday) === 'Mon'
? 'Monday'
: weekdayLabel(row.weekday) === 'Tue'
? 'Tuesday'
: weekdayLabel(row.weekday) === 'Wed'
? 'Wednesday'
: weekdayLabel(row.weekday) === 'Thu'
? 'Thursday'
: weekdayLabel(row.weekday) === 'Fri'
? 'Friday'
: weekdayLabel(row.weekday) === 'Sat'
? 'Saturday'
: 'Sunday'}
</span>
</div>
<span
class="inline-flex items-center rounded-full px-2.5 py-1 text-xs font-medium {row.is_open
? 'bg-emerald-100 text-emerald-800'
: 'bg-gray-100 text-gray-800'}"
>
<span
class="mr-1.5 h-1.5 w-1.5 rounded-full {row.is_open
? 'bg-emerald-600'
: 'bg-gray-600'}"
></span>
{row.is_open ? 'Open' : 'Closed'}
</span>
</div>
{#if row.is_open}
<div class="grid grid-cols-2 gap-3 text-sm">
<div>
<div class="mb-1 text-xs text-gray-500">Opening</div>
<div class="font-medium text-gray-900">{row.start_time}</div>
</div>
<div>
<div class="mb-1 text-xs text-gray-500">Closing</div>
<div class="font-medium text-gray-900">{row.end_time}</div>
</div>
</div>
<div class="mt-3 border-t pt-3 text-xs text-gray-600">
Total: <span class="font-medium text-gray-900"
>{calculateHours(row.start_time, row.end_time)} hours</span
>
</div>
{:else}
<div class="text-sm text-gray-500">No hours scheduled for this day</div>
{/if}
</div>
{/each}
</div>
</Card.Content>
{:else}
<!-- Skeleton loading -->
<Card.Content class="space-y-4">
<div class="w-full overflow-x-auto">
<table class="w-full table-auto">
<!-- Desktop Skeleton -->
<div class="hidden w-full overflow-x-auto md:block">
<table class="w-full table-auto border-collapse">
<thead>
<tr class="text-left text-xs text-gray-500">
<th class="py-2">Day</th>
<th class="py-2">Status</th>
<th class="py-2">Start</th>
<th class="py-2">End</th>
<tr
class="border-b bg-gray-50 text-left text-xs font-medium uppercase tracking-wider text-gray-600"
>
<th class="px-4 py-3">Day</th>
<th class="px-4 py-3 text-center">Status</th>
<th class="px-4 py-3">Opening Time</th>
<th class="px-4 py-3">Closing Time</th>
<th class="px-4 py-3 text-right">Total Hours</th>
</tr>
</thead>
<tbody>
<tbody class="divide-y divide-gray-200">
{#each Array(7) as _, 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>
<td class="px-4 py-4">
<div class="flex items-center gap-2">
<Skeleton class="h-8 w-8 rounded-full" />
<Skeleton class="h-4 w-20" />
</div>
</td>
<td class="px-4 py-4 text-center">
<Skeleton class="mx-auto h-6 w-16 rounded-full" />
</td>
<td class="px-4 py-4">
<Skeleton class="h-4 w-20" />
</td>
<td class="px-4 py-4">
<Skeleton class="h-4 w-20" />
</td>
<td class="px-4 py-4 text-right">
<Skeleton class="ml-auto h-4 w-12" />
</td>
</tr>
{/each}
</tbody>
</table>
</div>
<!-- Mobile Skeleton -->
<div class="space-y-3 md:hidden">
{#each Array(7) as _, i}
<div class="rounded-lg border p-4">
<div class="mb-3 flex items-center justify-between">
<div class="flex items-center gap-2">
<Skeleton class="h-10 w-10 rounded-full" />
<Skeleton class="h-5 w-24" />
</div>
<Skeleton class="h-6 w-16 rounded-full" />
</div>
<div class="grid grid-cols-2 gap-3">
<Skeleton class="h-12 w-full" />
<Skeleton class="h-12 w-full" />
</div>
</div>
{/each}
</div>
</Card.Content>
{/if}
</Card.Root>
<!-- Services Management Card -->
<Card.Root>
<Card.Header>
<Card.Title>Services Management</Card.Title>
<Card.Description
>Manage your services - add, edit, toggle availability, or delete services.</Card.Description
>
</Card.Header>
<Card.Content class="space-y-4">
<div class="flex items-center justify-between">
<Button onclick={openServiceModal}>Add Service</Button>
<div>
<Card.Title>Services Management</Card.Title>
<Card.Description>
Manage your services - add, edit, toggle availability, or delete services.
</Card.Description>
</div>
<Button onclick={openServiceModal}>
<svg
xmlns="http://www.w3.org/2000/svg"
class="mr-2 h-4 w-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<line x1="12" y1="5" x2="12" y2="19" />
<line x1="5" y1="12" x2="19" y2="12" />
</svg>
Add Service
</Button>
</div>
</Card.Header>
<Card.Content class="space-y-4">
<!-- Desktop Table -->
<div class="hidden w-full overflow-x-auto md:block">
<table class="w-full table-auto border-collapse text-sm">
@@ -1610,7 +1968,7 @@
<div class="w-full overflow-x-auto">
<table class="w-full table-auto">
<thead>
<tr class="text-left text-xs text-gray-500">
<tr class="border-b text-left text-xs text-gray-500">
<th class="py-2">Day</th>
<th class="py-2">Open</th>
<th class="py-2">Start</th>
+3 -3
View File
@@ -149,7 +149,7 @@ CREATE TABLE services (
requires_manual_pricing BOOLEAN NOT NULL DEFAULT FALSE,
requires_manual_duration BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_by CHAR(12),
created_by CHAR(12)
);
CREATE INDEX idx_services_name ON services(name);
@@ -175,7 +175,7 @@ CREATE TABLE bookings (
notes TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_by CHAR(12),
created_by CHAR(12)
);
CREATE INDEX idx_bookings_userid ON bookings(user_id);
@@ -275,7 +275,7 @@ CREATE TABLE payments (
net_amount NUMERIC(10,2),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_by CHAR(12),
created_by CHAR(12)
);
CREATE INDEX idx_payments_bookingid ON payments(booking_id);