Mobile optimise
This commit is contained in:
@@ -44,6 +44,7 @@
|
||||
fd.append('file', file);
|
||||
|
||||
try {
|
||||
// Note: API call is mocked here, replace with your actual endpoint
|
||||
// Mock success/fail
|
||||
await new Promise((r) => setTimeout(r, 500)); // Simulate network delay
|
||||
if (file.name.toLowerCase().includes('fail')) {
|
||||
@@ -142,7 +143,6 @@
|
||||
let editingException = $state<ExceptionGroup | null>(null);
|
||||
let editingExceptionIndex = $state<number | null>(null);
|
||||
let showExceptionModal = $state(false);
|
||||
let showUploadModal = $state(false); // Kept in case you want to toggle visibility
|
||||
|
||||
const dayNames = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
||||
|
||||
@@ -463,34 +463,34 @@
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<!-- Default Hours Card -->
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>Working Hours</Card.Title>
|
||||
<Card.Description>
|
||||
View default hours for each weekday, and manage exceptions.
|
||||
</Card.Description>
|
||||
<Card.Description>View and manage your standard weekly schedule.</Card.Description>
|
||||
</Card.Header>
|
||||
|
||||
<Card.Content class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="font-semibold">Default Hours</h3>
|
||||
<h3 class="font-semibold">Weekly Schedule</h3>
|
||||
<Button onclick={prepareDefaultHoursEdit} disabled={loadingHours || savingHours}>
|
||||
Edit Defaults
|
||||
</Button>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<div class="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">Day</th>
|
||||
<th class="py-2">Status</th> <th class="py-2">Start</th>
|
||||
<th class="py-2">Status</th>
|
||||
<th class="py-2">Start</th>
|
||||
<th class="py-2">End</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each defaultHours as row, i}
|
||||
{#each defaultHours as row}
|
||||
<tr class="border-t">
|
||||
<td class="py-2">{weekdayLabel(row.weekday)}</td>
|
||||
<td class="py-2 text-sm">{weekdayLabel(row.weekday)}</td>
|
||||
<td class="py-2">
|
||||
<span
|
||||
class="text-sm font-medium {row.is_open ? 'text-green-600' : 'text-red-600'}"
|
||||
@@ -498,34 +498,31 @@
|
||||
{row.is_open ? 'Open' : 'Closed'}
|
||||
</span>
|
||||
</td>
|
||||
<td class="py-2">
|
||||
<input
|
||||
class="rounded border bg-gray-100 px-2 py-1"
|
||||
type="time"
|
||||
value={row.start_time}
|
||||
disabled
|
||||
/>
|
||||
<td class="py-2 text-sm">
|
||||
{row.is_open ? row.start_time : '—'}
|
||||
</td>
|
||||
<td class="py-2">
|
||||
<input
|
||||
class="rounded border bg-gray-100 px-2 py-1"
|
||||
type="time"
|
||||
value={row.end_time}
|
||||
disabled
|
||||
/>
|
||||
<td class="py-2 text-sm">
|
||||
{row.is_open ? row.end_time : '—'}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<Separator />
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<!-- Exception Groups Card -->
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>Exception Groups</Card.Title>
|
||||
<Card.Description>
|
||||
Manage temporary schedules for holidays, closures, and special events.
|
||||
</Card.Description>
|
||||
</Card.Header>
|
||||
|
||||
<Card.Content class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 class="font-semibold">Exception Groups</h3>
|
||||
<p class="text-sm text-gray-500">Temporary schedules such as 'Christmas' or 'Holiday'</p>
|
||||
</div>
|
||||
<Button variant="outline" onclick={createNewException}>New Exception Group</Button>
|
||||
</div>
|
||||
|
||||
@@ -572,66 +569,67 @@
|
||||
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>Users & Bookings</Card.Title>
|
||||
<Card.Description>
|
||||
Search users or bookings. Click an item to view details and history.
|
||||
</Card.Description>
|
||||
<Card.Title>Users</Card.Title>
|
||||
<Card.Description>Search and manage user details.</Card.Description>
|
||||
</Card.Header>
|
||||
|
||||
<Card.Content class="space-y-4">
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
<Label>Search Users</Label>
|
||||
<div class="flex gap-2">
|
||||
<Input
|
||||
placeholder="Name, email or phone"
|
||||
bind:value={userQuery}
|
||||
onkeyup={(e) => {
|
||||
if ((e as KeyboardEvent).key === 'Enter') searchUsers();
|
||||
}}
|
||||
/>
|
||||
<Button onclick={searchUsers} disabled={loadingSearch}>Search</Button>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 max-h-60 space-y-2 overflow-y-auto">
|
||||
{#each users as u}
|
||||
<div class="flex items-center justify-between rounded bg-gray-50 p-2">
|
||||
<div>
|
||||
<div class="font-medium">{u.fn || `${u.n_first_name} ${u.n_last_name}`}</div>
|
||||
<div class="text-xs text-gray-500">{u.email} • {u.phone}</div>
|
||||
</div>
|
||||
<Button variant="outline" onclick={() => openUserModal(u.id)}>View</Button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex gap-2">
|
||||
<Input
|
||||
placeholder="Name, email or phone"
|
||||
bind:value={userQuery}
|
||||
onkeyup={(e) => {
|
||||
if ((e as KeyboardEvent).key === 'Enter') searchUsers();
|
||||
}}
|
||||
/>
|
||||
<Button onclick={searchUsers} disabled={loadingSearch}>Search</Button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Search Bookings</Label>
|
||||
<div class="flex gap-2">
|
||||
<Input
|
||||
placeholder="Booking ID or user"
|
||||
bind:value={bookingQuery}
|
||||
onkeyup={(e) => {
|
||||
if ((e as KeyboardEvent).key === 'Enter') searchBookings();
|
||||
}}
|
||||
/>
|
||||
<Button onclick={searchBookings} disabled={loadingSearch}>Search</Button>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 max-h-60 space-y-2 overflow-y-auto">
|
||||
{#each bookings as b}
|
||||
<div class="flex items-center justify-between rounded bg-gray-50 p-2">
|
||||
<div>
|
||||
<div class="font-medium">{new Date(b.start_time).toLocaleString()}</div>
|
||||
<div class="text-xs text-gray-500">
|
||||
{b.status} • {b.services?.map((s) => s.name).join(', ')}
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="outline" onclick={() => openBookingModal(b.id)}>View</Button>
|
||||
<div class="mt-3 max-h-60 space-y-2 overflow-y-auto">
|
||||
{#each users as u}
|
||||
<div class="flex items-center justify-between rounded bg-gray-50 p-2">
|
||||
<div>
|
||||
<div class="font-medium">{u.fn || `${u.n_first_name} ${u.n_last_name}`}</div>
|
||||
<div class="text-xs text-gray-500">{u.email} • {u.phone}</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<Button variant="outline" onclick={() => openUserModal(u.id)}>View</Button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>Bookings</Card.Title>
|
||||
<Card.Description>Search and manage booking history.</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content class="space-y-4">
|
||||
<div>
|
||||
<div class="flex gap-2">
|
||||
<Input
|
||||
placeholder="Booking ID or user"
|
||||
bind:value={bookingQuery}
|
||||
onkeyup={(e) => {
|
||||
if ((e as KeyboardEvent).key === 'Enter') searchBookings();
|
||||
}}
|
||||
/>
|
||||
<Button onclick={searchBookings} disabled={loadingSearch}>Search</Button>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 max-h-60 space-y-2 overflow-y-auto">
|
||||
{#each bookings as b}
|
||||
<div class="flex items-center justify-between rounded bg-gray-50 p-2">
|
||||
<div>
|
||||
<div class="font-medium">{new Date(b.start_time).toLocaleString()}</div>
|
||||
<div class="text-xs text-gray-500">
|
||||
{b.status} • {b.services?.map((s) => s.name).join(', ')}
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="outline" onclick={() => openBookingModal(b.id)}>View</Button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</Card.Content>
|
||||
@@ -639,7 +637,7 @@
|
||||
</div>
|
||||
|
||||
<Modal.Root bind:open={showDefaultHoursModal}>
|
||||
<Modal.Content>
|
||||
<Modal.Content class="max-h-[90vh] max-w-sm overflow-y-auto md:max-w-lg">
|
||||
<Modal.Header>
|
||||
<Modal.Title class="text-lg font-semibold">Edit Default Working Hours</Modal.Title>
|
||||
<Modal.Description>
|
||||
@@ -648,7 +646,7 @@
|
||||
</Modal.Header>
|
||||
|
||||
<div class="px-4 pb-4">
|
||||
<div class="overflow-x-auto">
|
||||
<div class="w-full overflow-x-auto">
|
||||
<table class="w-full table-auto">
|
||||
<thead>
|
||||
<tr class="text-left text-xs text-gray-500">
|
||||
@@ -661,7 +659,7 @@
|
||||
<tbody>
|
||||
{#each defaultHoursDraft as row}
|
||||
<tr class="border-t">
|
||||
<td class="py-2">{weekdayLabel(row.weekday)}</td>
|
||||
<td class="py-2 text-sm">{weekdayLabel(row.weekday)}</td>
|
||||
<td class="py-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -674,7 +672,7 @@
|
||||
type="time"
|
||||
bind:value={row.start_time}
|
||||
disabled={!row.is_open}
|
||||
class="max-w-24"
|
||||
class="max-w-[70px] text-sm"
|
||||
/>
|
||||
</td>
|
||||
<td class="py-2">
|
||||
@@ -682,7 +680,7 @@
|
||||
type="time"
|
||||
bind:value={row.end_time}
|
||||
disabled={!row.is_open}
|
||||
class="max-w-24"
|
||||
class="max-w-[70px] text-sm"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -709,15 +707,15 @@
|
||||
</Modal.Root>
|
||||
|
||||
<Modal.Root bind:open={showExceptionModal}>
|
||||
<Modal.Content>
|
||||
<Modal.Header>
|
||||
<Modal.Content class="max-h-[90vh] max-w-sm overflow-y-auto p-4 md:max-w-lg">
|
||||
<Modal.Header class="mb-4 p-0">
|
||||
<Modal.Title class="text-lg font-semibold">
|
||||
{editingExceptionIndex === null ? 'New Exception Group' : 'Edit Exception Group'}
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
|
||||
{#if editingException}
|
||||
<div class="space-y-4 px-4 pb-4">
|
||||
<div class="space-y-4 pb-4">
|
||||
<div>
|
||||
<Label>Name</Label>
|
||||
<Input bind:value={editingException.name} placeholder="e.g. Christmas" />
|
||||
@@ -727,16 +725,17 @@
|
||||
<Textarea
|
||||
rows={3}
|
||||
bind:value={editingException.description}
|
||||
placeholder="A short description of the exception"
|
||||
placeholder="e.g. Extended weekend hours and shortened weekday hours for the holiday period"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Week range to apply</Label>
|
||||
<div class="flex gap-2">
|
||||
<Input type="date" id="exception_from" />
|
||||
<Input type="date" id="exception_to" />
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<Input type="date" id="exception_from" class="min-w-[120px] flex-1" />
|
||||
<Input type="date" id="exception_to" class="min-w-[120px] flex-1" />
|
||||
<Button
|
||||
class="w-full sm:w-auto"
|
||||
onclick={() => {
|
||||
const fromEl = document.getElementById('exception_from') as HTMLInputElement;
|
||||
const toEl = document.getElementById('exception_to') as HTMLInputElement;
|
||||
@@ -792,9 +791,14 @@
|
||||
type="time"
|
||||
bind:value={r.start_time}
|
||||
disabled={!r.is_open}
|
||||
class="max-w-24"
|
||||
class="max-w-[70px] text-sm"
|
||||
/>
|
||||
<Input
|
||||
type="time"
|
||||
bind:value={r.end_time}
|
||||
disabled={!r.is_open}
|
||||
class="max-w-[70px] text-sm"
|
||||
/>
|
||||
<Input type="time" bind:value={r.end_time} disabled={!r.is_open} class="max-w-24" />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -802,7 +806,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Modal.Footer class="flex items-center justify-end gap-2">
|
||||
<Modal.Footer class="flex items-center justify-end gap-2 p-0 pt-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
onclick={() => {
|
||||
@@ -819,49 +823,9 @@
|
||||
</Modal.Content>
|
||||
</Modal.Root>
|
||||
|
||||
<Modal.Root bind:open={showUploadModal}>
|
||||
<Modal.Content>
|
||||
<Modal.Header>
|
||||
<Modal.Title class="text-lg font-semibold">Upload Images</Modal.Title>
|
||||
</Modal.Header>
|
||||
|
||||
<div class="px-4 pb-4">
|
||||
<FileDropZone onfiles={handleFilesDropped} accept="image/*" multiple>
|
||||
<div class="p-6 text-center">
|
||||
<p class="text-sm text-gray-500">Drop files here, or click to open the file picker</p>
|
||||
</div>
|
||||
</FileDropZone>
|
||||
|
||||
<div class="mt-4">
|
||||
<div class="text-sm text-gray-600">Selected files</div>
|
||||
<div class="mt-2 space-y-2">
|
||||
{#each uploadFiles as f}
|
||||
<div class="flex items-center justify-between rounded bg-gray-50 p-2">
|
||||
<div>{f.name} • {Math.round(f.size / 1024)}KB</div>
|
||||
<button
|
||||
class="text-red-500"
|
||||
onclick={() => (uploadFiles = uploadFiles.filter((x) => x !== f))}
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal.Footer class="flex items-center justify-end gap-2">
|
||||
<Button variant="outline" onclick={() => (showUploadModal = false)}>Close</Button>
|
||||
<Button onclick={uploadOneOrMany} disabled={!uploadFiles.length || uploading}>
|
||||
{uploading ? `Uploading (${uploadProgress}%)` : 'Upload'}
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal.Content>
|
||||
</Modal.Root>
|
||||
|
||||
{#if selectedUser}
|
||||
<Modal.Root bind:open={showUserModal}>
|
||||
<Modal.Content>
|
||||
<Modal.Content class="max-h-[90vh] max-w-sm overflow-y-auto md:max-w-lg">
|
||||
<Modal.Header>
|
||||
<Modal.Title class="text-lg font-semibold">
|
||||
User: {selectedUser.fn || `${selectedUser.n_first_name} ${selectedUser.n_last_name}`}
|
||||
@@ -936,7 +900,7 @@
|
||||
|
||||
{#if selectedBooking}
|
||||
<Modal.Root bind:open={showBookingModal}>
|
||||
<Modal.Content>
|
||||
<Modal.Content class="max-h-[90vh] max-w-sm overflow-y-auto md:max-w-lg">
|
||||
<Modal.Header>
|
||||
<Modal.Title class="text-lg font-semibold">Booking {selectedBooking.id}</Modal.Title>
|
||||
</Modal.Header>
|
||||
|
||||
Reference in New Issue
Block a user