Mobile optimise
This commit is contained in:
@@ -44,6 +44,7 @@
|
|||||||
fd.append('file', file);
|
fd.append('file', file);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Note: API call is mocked here, replace with your actual endpoint
|
||||||
// Mock success/fail
|
// Mock success/fail
|
||||||
await new Promise((r) => setTimeout(r, 500)); // Simulate network delay
|
await new Promise((r) => setTimeout(r, 500)); // Simulate network delay
|
||||||
if (file.name.toLowerCase().includes('fail')) {
|
if (file.name.toLowerCase().includes('fail')) {
|
||||||
@@ -142,7 +143,6 @@
|
|||||||
let editingException = $state<ExceptionGroup | null>(null);
|
let editingException = $state<ExceptionGroup | null>(null);
|
||||||
let editingExceptionIndex = $state<number | null>(null);
|
let editingExceptionIndex = $state<number | null>(null);
|
||||||
let showExceptionModal = $state(false);
|
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'];
|
const dayNames = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
||||||
|
|
||||||
@@ -463,34 +463,34 @@
|
|||||||
</Card.Content>
|
</Card.Content>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
|
|
||||||
|
<!-- Default Hours Card -->
|
||||||
<Card.Root>
|
<Card.Root>
|
||||||
<Card.Header>
|
<Card.Header>
|
||||||
<Card.Title>Working Hours</Card.Title>
|
<Card.Title>Working Hours</Card.Title>
|
||||||
<Card.Description>
|
<Card.Description>View and manage your standard weekly schedule.</Card.Description>
|
||||||
View default hours for each weekday, and manage exceptions.
|
|
||||||
</Card.Description>
|
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
|
|
||||||
<Card.Content class="space-y-4">
|
<Card.Content class="space-y-4">
|
||||||
<div class="flex items-center justify-between">
|
<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}>
|
<Button onclick={prepareDefaultHoursEdit} disabled={loadingHours || savingHours}>
|
||||||
Edit Defaults
|
Edit Defaults
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div class="overflow-x-auto">
|
<div class="w-full overflow-x-auto">
|
||||||
<table class="w-full table-auto">
|
<table class="w-full table-auto">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="text-left text-xs text-gray-500">
|
<tr class="text-left text-xs text-gray-500">
|
||||||
<th class="py-2">Day</th>
|
<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>
|
<th class="py-2">End</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each defaultHours as row, i}
|
{#each defaultHours as row}
|
||||||
<tr class="border-t">
|
<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">
|
<td class="py-2">
|
||||||
<span
|
<span
|
||||||
class="text-sm font-medium {row.is_open ? 'text-green-600' : 'text-red-600'}"
|
class="text-sm font-medium {row.is_open ? 'text-green-600' : 'text-red-600'}"
|
||||||
@@ -498,34 +498,31 @@
|
|||||||
{row.is_open ? 'Open' : 'Closed'}
|
{row.is_open ? 'Open' : 'Closed'}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="py-2">
|
<td class="py-2 text-sm">
|
||||||
<input
|
{row.is_open ? row.start_time : '—'}
|
||||||
class="rounded border bg-gray-100 px-2 py-1"
|
|
||||||
type="time"
|
|
||||||
value={row.start_time}
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
</td>
|
</td>
|
||||||
<td class="py-2">
|
<td class="py-2 text-sm">
|
||||||
<input
|
{row.is_open ? row.end_time : '—'}
|
||||||
class="rounded border bg-gray-100 px-2 py-1"
|
|
||||||
type="time"
|
|
||||||
value={row.end_time}
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
{/each}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</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 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>
|
<Button variant="outline" onclick={createNewException}>New Exception Group</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -572,66 +569,67 @@
|
|||||||
|
|
||||||
<Card.Root>
|
<Card.Root>
|
||||||
<Card.Header>
|
<Card.Header>
|
||||||
<Card.Title>Users & Bookings</Card.Title>
|
<Card.Title>Users</Card.Title>
|
||||||
<Card.Description>
|
<Card.Description>Search and manage user details.</Card.Description>
|
||||||
Search users or bookings. Click an item to view details and history.
|
|
||||||
</Card.Description>
|
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
|
|
||||||
<Card.Content class="space-y-4">
|
<Card.Content class="space-y-4">
|
||||||
<div class="grid gap-4 md:grid-cols-2">
|
<div>
|
||||||
<div>
|
<div class="flex gap-2">
|
||||||
<Label>Search Users</Label>
|
<Input
|
||||||
<div class="flex gap-2">
|
placeholder="Name, email or phone"
|
||||||
<Input
|
bind:value={userQuery}
|
||||||
placeholder="Name, email or phone"
|
onkeyup={(e) => {
|
||||||
bind:value={userQuery}
|
if ((e as KeyboardEvent).key === 'Enter') searchUsers();
|
||||||
onkeyup={(e) => {
|
}}
|
||||||
if ((e as KeyboardEvent).key === 'Enter') searchUsers();
|
/>
|
||||||
}}
|
<Button onclick={searchUsers} disabled={loadingSearch}>Search</Button>
|
||||||
/>
|
|
||||||
<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>
|
||||||
|
|
||||||
<div>
|
<div class="mt-3 max-h-60 space-y-2 overflow-y-auto">
|
||||||
<Label>Search Bookings</Label>
|
{#each users as u}
|
||||||
<div class="flex gap-2">
|
<div class="flex items-center justify-between rounded bg-gray-50 p-2">
|
||||||
<Input
|
<div>
|
||||||
placeholder="Booking ID or user"
|
<div class="font-medium">{u.fn || `${u.n_first_name} ${u.n_last_name}`}</div>
|
||||||
bind:value={bookingQuery}
|
<div class="text-xs text-gray-500">{u.email} • {u.phone}</div>
|
||||||
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>
|
</div>
|
||||||
{/each}
|
<Button variant="outline" onclick={() => openUserModal(u.id)}>View</Button>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
@@ -639,7 +637,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Modal.Root bind:open={showDefaultHoursModal}>
|
<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.Header>
|
||||||
<Modal.Title class="text-lg font-semibold">Edit Default Working Hours</Modal.Title>
|
<Modal.Title class="text-lg font-semibold">Edit Default Working Hours</Modal.Title>
|
||||||
<Modal.Description>
|
<Modal.Description>
|
||||||
@@ -648,7 +646,7 @@
|
|||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
|
|
||||||
<div class="px-4 pb-4">
|
<div class="px-4 pb-4">
|
||||||
<div class="overflow-x-auto">
|
<div class="w-full overflow-x-auto">
|
||||||
<table class="w-full table-auto">
|
<table class="w-full table-auto">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="text-left text-xs text-gray-500">
|
<tr class="text-left text-xs text-gray-500">
|
||||||
@@ -661,7 +659,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{#each defaultHoursDraft as row}
|
{#each defaultHoursDraft as row}
|
||||||
<tr class="border-t">
|
<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">
|
<td class="py-2">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -674,7 +672,7 @@
|
|||||||
type="time"
|
type="time"
|
||||||
bind:value={row.start_time}
|
bind:value={row.start_time}
|
||||||
disabled={!row.is_open}
|
disabled={!row.is_open}
|
||||||
class="max-w-24"
|
class="max-w-[70px] text-sm"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td class="py-2">
|
<td class="py-2">
|
||||||
@@ -682,7 +680,7 @@
|
|||||||
type="time"
|
type="time"
|
||||||
bind:value={row.end_time}
|
bind:value={row.end_time}
|
||||||
disabled={!row.is_open}
|
disabled={!row.is_open}
|
||||||
class="max-w-24"
|
class="max-w-[70px] text-sm"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -709,15 +707,15 @@
|
|||||||
</Modal.Root>
|
</Modal.Root>
|
||||||
|
|
||||||
<Modal.Root bind:open={showExceptionModal}>
|
<Modal.Root bind:open={showExceptionModal}>
|
||||||
<Modal.Content>
|
<Modal.Content class="max-h-[90vh] max-w-sm overflow-y-auto p-4 md:max-w-lg">
|
||||||
<Modal.Header>
|
<Modal.Header class="mb-4 p-0">
|
||||||
<Modal.Title class="text-lg font-semibold">
|
<Modal.Title class="text-lg font-semibold">
|
||||||
{editingExceptionIndex === null ? 'New Exception Group' : 'Edit Exception Group'}
|
{editingExceptionIndex === null ? 'New Exception Group' : 'Edit Exception Group'}
|
||||||
</Modal.Title>
|
</Modal.Title>
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
|
|
||||||
{#if editingException}
|
{#if editingException}
|
||||||
<div class="space-y-4 px-4 pb-4">
|
<div class="space-y-4 pb-4">
|
||||||
<div>
|
<div>
|
||||||
<Label>Name</Label>
|
<Label>Name</Label>
|
||||||
<Input bind:value={editingException.name} placeholder="e.g. Christmas" />
|
<Input bind:value={editingException.name} placeholder="e.g. Christmas" />
|
||||||
@@ -727,16 +725,17 @@
|
|||||||
<Textarea
|
<Textarea
|
||||||
rows={3}
|
rows={3}
|
||||||
bind:value={editingException.description}
|
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>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label>Week range to apply</Label>
|
<Label>Week range to apply</Label>
|
||||||
<div class="flex gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
<Input type="date" id="exception_from" />
|
<Input type="date" id="exception_from" class="min-w-[120px] flex-1" />
|
||||||
<Input type="date" id="exception_to" />
|
<Input type="date" id="exception_to" class="min-w-[120px] flex-1" />
|
||||||
<Button
|
<Button
|
||||||
|
class="w-full sm:w-auto"
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
const fromEl = document.getElementById('exception_from') as HTMLInputElement;
|
const fromEl = document.getElementById('exception_from') as HTMLInputElement;
|
||||||
const toEl = document.getElementById('exception_to') as HTMLInputElement;
|
const toEl = document.getElementById('exception_to') as HTMLInputElement;
|
||||||
@@ -792,9 +791,14 @@
|
|||||||
type="time"
|
type="time"
|
||||||
bind:value={r.start_time}
|
bind:value={r.start_time}
|
||||||
disabled={!r.is_open}
|
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>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
@@ -802,7 +806,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/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
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
@@ -819,49 +823,9 @@
|
|||||||
</Modal.Content>
|
</Modal.Content>
|
||||||
</Modal.Root>
|
</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}
|
{#if selectedUser}
|
||||||
<Modal.Root bind:open={showUserModal}>
|
<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.Header>
|
||||||
<Modal.Title class="text-lg font-semibold">
|
<Modal.Title class="text-lg font-semibold">
|
||||||
User: {selectedUser.fn || `${selectedUser.n_first_name} ${selectedUser.n_last_name}`}
|
User: {selectedUser.fn || `${selectedUser.n_first_name} ${selectedUser.n_last_name}`}
|
||||||
@@ -936,7 +900,7 @@
|
|||||||
|
|
||||||
{#if selectedBooking}
|
{#if selectedBooking}
|
||||||
<Modal.Root bind:open={showBookingModal}>
|
<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.Header>
|
||||||
<Modal.Title class="text-lg font-semibold">Booking {selectedBooking.id}</Modal.Title>
|
<Modal.Title class="text-lg font-semibold">Booking {selectedBooking.id}</Modal.Title>
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
|
|||||||
Reference in New Issue
Block a user