679 lines
20 KiB
Svelte
679 lines
20 KiB
Svelte
<script lang="ts">
|
|
import { authStore } from '$lib/stores/auth.svelte';
|
|
import { SvelteDate } from 'svelte/reactivity';
|
|
import { toast } from 'svelte-sonner';
|
|
|
|
// shadcn-svelte components
|
|
import { Button } from '$lib/components/ui/button';
|
|
import * as Card from '$lib/components/ui/card';
|
|
import { Input } from '$lib/components/ui/input';
|
|
import { Separator } from '$lib/components/ui/separator';
|
|
import * as Modal from '$lib/components/ui/dialog';
|
|
import * as AlertDialog from '$lib/components/ui/alert-dialog';
|
|
import { Skeleton } from '$lib/components/ui/skeleton';
|
|
|
|
// =============== Types ===============
|
|
type WorkingHourRow = {
|
|
weekday: number;
|
|
start_time: string;
|
|
end_time: string;
|
|
is_open: boolean;
|
|
};
|
|
|
|
type ExceptionGroup = {
|
|
id?: number;
|
|
name: string;
|
|
description: string;
|
|
weekStarts: string[];
|
|
hours: WorkingHourRow[];
|
|
};
|
|
|
|
// =============== State ===============
|
|
let exceptionGroups = $state<ExceptionGroup[]>([]);
|
|
let exceptionGroupsLoading = $state(true);
|
|
let savingHours = $state(false);
|
|
|
|
// Exception modal state
|
|
let showExceptionModal = $state(false);
|
|
let exceptionDraft = $state<ExceptionGroup>({
|
|
name: '',
|
|
description: '',
|
|
weekStarts: [],
|
|
hours: [
|
|
{ weekday: 0, start_time: '00:00', end_time: '00:00', is_open: false },
|
|
{ weekday: 1, start_time: '09:00', end_time: '17:00', is_open: true },
|
|
{ weekday: 2, start_time: '09:00', end_time: '17:00', is_open: true },
|
|
{ weekday: 3, start_time: '09:00', end_time: '17:00', is_open: true },
|
|
{ weekday: 4, start_time: '12:00', end_time: '20:00', is_open: true },
|
|
{ weekday: 5, start_time: '09:00', end_time: '17:00', is_open: true },
|
|
{ weekday: 6, start_time: '00:00', end_time: '00:00', is_open: false }
|
|
]
|
|
});
|
|
|
|
let weekRangeFrom = $state('');
|
|
let weekRangeTo = $state('');
|
|
|
|
// Delete confirmation state
|
|
let showDeleteExceptionAlert = $state(false);
|
|
let exceptionToDelete = $state<number | undefined>(undefined);
|
|
|
|
// View exception state
|
|
let showViewExceptionModal = $state(false);
|
|
let viewingException = $state<ExceptionGroup | null>(null);
|
|
|
|
const dayNames = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
|
|
|
// =============== Helper Functions ===============
|
|
function weekdayLabel(i: number) {
|
|
return dayNames[i];
|
|
}
|
|
|
|
function isoDateOf(d: Date) {
|
|
return d.toISOString().slice(0, 10);
|
|
}
|
|
|
|
function addWeeksToException(fromISO: string, toISO: string, dest: string[]) {
|
|
const from = new SvelteDate(fromISO + 'T00:00:00');
|
|
const to = new SvelteDate(toISO + 'T00:00:00');
|
|
const first = new SvelteDate(from);
|
|
const day = first.getDay();
|
|
const daysToMonday = day === 0 ? -6 : 1 - day;
|
|
|
|
// Set to the Monday of the current week
|
|
first.setDate(first.getDate() + daysToMonday);
|
|
|
|
// Add all Mondays in the range
|
|
for (let d = new SvelteDate(first); d <= to; d.setDate(d.getDate() + 7)) {
|
|
dest.push(isoDateOf(new SvelteDate(d)));
|
|
}
|
|
}
|
|
|
|
/** Format time from HH:MM:SS to 12-hour format, with "Noon" for 12:00 PM */
|
|
function formatTime(time: string): string {
|
|
const [hours, minutes] = time.split(':').map(Number);
|
|
|
|
// Special case for 12:00
|
|
if (hours === 12 && minutes === 0) {
|
|
return 'Noon';
|
|
} else if (hours === 0 && minutes === 0) {
|
|
return 'Midnight';
|
|
}
|
|
|
|
const period = hours >= 12 ? 'PM' : 'AM';
|
|
const displayHours = hours % 12 || 12;
|
|
return `${displayHours}:${minutes.toString().padStart(2, '0')} ${period}`;
|
|
}
|
|
|
|
// =============== API Functions ===============
|
|
async function fetchExceptionGroups() {
|
|
exceptionGroupsLoading = true;
|
|
try {
|
|
const response = await fetch('/api/scheduling/exceptional-groups', {
|
|
method: 'GET',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
Authorization: `Bearer ${authStore.currentToken}`
|
|
}
|
|
});
|
|
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
if (data === null || data.length === 0) {
|
|
return;
|
|
}
|
|
exceptionGroups = data.map((group) => ({
|
|
id: group.id,
|
|
name: group.name,
|
|
description: group.description,
|
|
weekStarts: group.weekStarts || [],
|
|
hours:
|
|
group.hours?.map((h) => ({
|
|
id: h.id,
|
|
weekday: h.weekday,
|
|
start_time: formatTime(h.startTime),
|
|
end_time: formatTime(h.endTime),
|
|
is_open: h.isOpen
|
|
})) || []
|
|
}));
|
|
} else {
|
|
console.error('Failed to fetch exception groups:', response.status);
|
|
toast.error('Failed to load exception groups');
|
|
}
|
|
} catch (err) {
|
|
console.error('Error fetching exception groups:', err);
|
|
toast.error('Network error loading exception groups');
|
|
} finally {
|
|
exceptionGroupsLoading = false;
|
|
}
|
|
}
|
|
|
|
async function saveExceptionGroup() {
|
|
// Validate
|
|
if (!exceptionDraft.name.trim()) {
|
|
toast.error('Please enter a group name');
|
|
return;
|
|
}
|
|
|
|
if (exceptionDraft.weekStarts.length === 0) {
|
|
toast.error('Please add at least one week');
|
|
return;
|
|
}
|
|
|
|
savingHours = true;
|
|
const loadingToast = toast.loading('Creating exception group...');
|
|
|
|
try {
|
|
// Map to API format
|
|
const payload = {
|
|
name: exceptionDraft.name,
|
|
description: exceptionDraft.description,
|
|
weekStarts: exceptionDraft.weekStarts,
|
|
hours: exceptionDraft.hours.map((h) => ({
|
|
weekday: h.weekday,
|
|
startTime: h.start_time,
|
|
endTime: h.end_time,
|
|
isOpen: h.is_open
|
|
}))
|
|
};
|
|
|
|
const response = await fetch('/api/scheduling/exceptional-groups', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
Authorization: `Bearer ${authStore.currentToken}`
|
|
},
|
|
body: JSON.stringify(payload)
|
|
});
|
|
|
|
if (response.ok) {
|
|
toast.success('Exception group created successfully!', { id: loadingToast });
|
|
showExceptionModal = false;
|
|
resetExceptionForm();
|
|
await fetchExceptionGroups();
|
|
} else {
|
|
const text = await response.text();
|
|
toast.error('Failed to create: ' + text, { id: loadingToast });
|
|
}
|
|
} catch (err) {
|
|
console.error('Error creating exception group:', err);
|
|
toast.error('Network error creating exception group', { id: loadingToast });
|
|
} finally {
|
|
savingHours = false;
|
|
}
|
|
}
|
|
|
|
async function confirmDeleteExceptionGroup() {
|
|
if (exceptionToDelete === undefined) return;
|
|
|
|
const loadingToast = toast.loading('Deleting exception group...');
|
|
|
|
try {
|
|
const response = await fetch(`/api/scheduling/exceptional-groups?id=${exceptionToDelete}`, {
|
|
method: 'DELETE',
|
|
headers: {
|
|
Authorization: `Bearer ${authStore.currentToken}`
|
|
}
|
|
});
|
|
|
|
if (response.ok || response.status === 204) {
|
|
toast.success('Exception group deleted successfully!', { id: loadingToast });
|
|
showDeleteExceptionAlert = false;
|
|
exceptionToDelete = undefined;
|
|
// Refresh the exception groups list
|
|
await fetchExceptionGroups();
|
|
} else {
|
|
const text = await response.text();
|
|
toast.error('Failed to delete: ' + text, { id: loadingToast });
|
|
}
|
|
} catch (err) {
|
|
console.error('Error deleting exception group:', err);
|
|
toast.error('Network error deleting exception group', { id: loadingToast });
|
|
}
|
|
}
|
|
|
|
// =============== UI Actions ===============
|
|
function resetExceptionForm() {
|
|
exceptionDraft = {
|
|
name: '',
|
|
description: '',
|
|
weekStarts: [],
|
|
hours: [
|
|
{ weekday: 0, start_time: '00:00', end_time: '00:00', is_open: false },
|
|
{ weekday: 1, start_time: '09:00', end_time: '17:00', is_open: true },
|
|
{ weekday: 2, start_time: '09:00', end_time: '17:00', is_open: true },
|
|
{ weekday: 3, start_time: '09:00', end_time: '17:00', is_open: true },
|
|
{ weekday: 4, start_time: '12:00', end_time: '20:00', is_open: true },
|
|
{ weekday: 5, start_time: '09:00', end_time: '17:00', is_open: true },
|
|
{ weekday: 6, start_time: '00:00', end_time: '00:00', is_open: false }
|
|
]
|
|
};
|
|
weekRangeFrom = '';
|
|
weekRangeTo = '';
|
|
}
|
|
|
|
function createNewException() {
|
|
resetExceptionForm();
|
|
showExceptionModal = true;
|
|
}
|
|
|
|
function addWeekRange() {
|
|
if (!weekRangeFrom || !weekRangeTo) {
|
|
toast.error('Please select both start and end dates');
|
|
return;
|
|
}
|
|
|
|
addWeeksToException(weekRangeFrom, weekRangeTo, exceptionDraft.weekStarts);
|
|
weekRangeFrom = '';
|
|
weekRangeTo = '';
|
|
}
|
|
|
|
function removeWeek(index: number) {
|
|
exceptionDraft.weekStarts = exceptionDraft.weekStarts.filter((_, i) => i !== index);
|
|
}
|
|
|
|
function openViewExceptionModal(exception: ExceptionGroup) {
|
|
viewingException = exception;
|
|
showViewExceptionModal = true;
|
|
}
|
|
|
|
// =============== Effects ===============
|
|
$effect(() => {
|
|
fetchExceptionGroups();
|
|
});
|
|
</script>
|
|
|
|
<Card.Root>
|
|
<Card.Header>
|
|
<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">
|
|
{#if exceptionGroupsLoading}
|
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
{#each Array(2) as _, i (i)}
|
|
<Skeleton class="h-32 w-full" />
|
|
{/each}
|
|
</div>
|
|
{:else}
|
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
{#if exceptionGroups.length === 0}
|
|
<p class="col-span-2 text-sm text-gray-500">No exception groups found.</p>
|
|
{/if}
|
|
|
|
{#each exceptionGroups as g (g.weekStarts)}
|
|
<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="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 SvelteDate(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="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"
|
|
size="sm"
|
|
onclick={() => {
|
|
exceptionToDelete = g.id;
|
|
showDeleteExceptionAlert = true;
|
|
}}
|
|
>
|
|
<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>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
</Card.Content>
|
|
</Card.Root>
|
|
|
|
<!-- Exception Group Modal -->
|
|
<Modal.Root bind:open={showExceptionModal}>
|
|
<Modal.Content class="max-h-[90vh] max-w-2xl overflow-y-auto">
|
|
<Modal.Header>
|
|
<Modal.Title class="text-lg font-semibold">Create Exception Schedule</Modal.Title>
|
|
<Modal.Description>
|
|
Define custom working hours for holidays, closures, or special events.
|
|
</Modal.Description>
|
|
</Modal.Header>
|
|
|
|
<div class="space-y-6 px-4 pb-4">
|
|
<!-- Basic Info -->
|
|
<div class="space-y-4">
|
|
<div class="space-y-2">
|
|
<label for="exception-name" class="text-sm font-medium">Schedule Name *</label>
|
|
<Input
|
|
id="exception-name"
|
|
type="text"
|
|
placeholder="e.g., Christmas Week, Summer Holiday"
|
|
bind:value={exceptionDraft.name}
|
|
/>
|
|
</div>
|
|
|
|
<div class="space-y-2">
|
|
<label for="exception-description" class="text-sm font-medium">Description</label>
|
|
<Input
|
|
id="exception-description"
|
|
type="text"
|
|
placeholder="Brief description of the service, will be shown to customers"
|
|
bind:value={exceptionDraft.description}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
<!-- Week Selection -->
|
|
<div class="space-y-4">
|
|
<div>
|
|
<h3 class="mb-2 text-sm font-medium">Apply to Weeks *</h3>
|
|
<p class="mb-3 text-xs text-gray-500">
|
|
Select a date range to add all Mondays within that range
|
|
</p>
|
|
|
|
<div class="grid grid-cols-1 gap-3 md:grid-cols-2">
|
|
<div class="space-y-2">
|
|
<label for="week-from" class="text-xs text-gray-600">From Date</label>
|
|
<Input id="week-from" type="date" bind:value={weekRangeFrom} />
|
|
</div>
|
|
<div class="space-y-2">
|
|
<label for="week-to" class="text-xs text-gray-600">To Date</label>
|
|
<Input id="week-to" type="date" bind:value={weekRangeTo} />
|
|
</div>
|
|
</div>
|
|
|
|
<Button variant="outline" size="sm" onclick={addWeekRange} class="mt-3">
|
|
Add Week Range
|
|
</Button>
|
|
</div>
|
|
|
|
{#if exceptionDraft.weekStarts.length > 0}
|
|
<div class="space-y-2">
|
|
<div class="text-xs text-gray-600">
|
|
Selected weeks ({exceptionDraft.weekStarts.length}):
|
|
</div>
|
|
<div class="max-h-32 space-y-1 overflow-y-auto rounded border p-2">
|
|
{#each exceptionDraft.weekStarts as week, index (week)}
|
|
<div class="flex items-center justify-between text-sm">
|
|
<span>Week starting: {week}</span>
|
|
<button
|
|
class="text-xs text-red-500 hover:text-red-700"
|
|
onclick={() => removeWeek(index)}
|
|
>
|
|
Remove
|
|
</button>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
<!-- Working Hours -->
|
|
<div class="space-y-4">
|
|
<h3 class="text-sm font-medium">Working Hours for these Weeks *</h3>
|
|
<div class="w-full overflow-x-auto">
|
|
<table class="w-full table-auto text-sm">
|
|
<thead>
|
|
<tr class="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>
|
|
<th class="py-2">End</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{#each exceptionDraft.hours as row (row.weekday)}
|
|
<tr class="border-t">
|
|
<td class="py-2">{weekdayLabel(row.weekday)}</td>
|
|
<td class="py-2">
|
|
<input
|
|
type="checkbox"
|
|
bind:checked={row.is_open}
|
|
class="h-4 w-4 rounded border-gray-300 bg-gray-100"
|
|
/>
|
|
</td>
|
|
<td class="py-2">
|
|
<Input
|
|
type="time"
|
|
bind:value={row.start_time}
|
|
disabled={!row.is_open}
|
|
class="w-24 text-sm"
|
|
/>
|
|
</td>
|
|
<td class="py-2">
|
|
<Input
|
|
type="time"
|
|
bind:value={row.end_time}
|
|
disabled={!row.is_open}
|
|
class="w-24 text-sm"
|
|
/>
|
|
</td>
|
|
</tr>
|
|
{/each}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<Modal.Footer class="flex items-center justify-end gap-2">
|
|
<Button
|
|
variant="outline"
|
|
onclick={() => {
|
|
showExceptionModal = false;
|
|
resetExceptionForm();
|
|
}}
|
|
disabled={savingHours}
|
|
>
|
|
Cancel
|
|
</Button>
|
|
<Button onclick={saveExceptionGroup} disabled={savingHours}>
|
|
{savingHours ? 'Creating…' : 'Create Schedule'}
|
|
</Button>
|
|
</Modal.Footer>
|
|
</Modal.Content>
|
|
</Modal.Root>
|
|
|
|
<!-- Delete Exception Confirmation -->
|
|
<AlertDialog.Root bind:open={showDeleteExceptionAlert}>
|
|
<AlertDialog.Content class="z-[60]">
|
|
<AlertDialog.Header>
|
|
<AlertDialog.Title>Delete exception group?</AlertDialog.Title>
|
|
<AlertDialog.Description>
|
|
This action cannot be undone. This will permanently delete this exception group and all its
|
|
associated schedule rows.
|
|
</AlertDialog.Description>
|
|
</AlertDialog.Header>
|
|
<AlertDialog.Footer>
|
|
<AlertDialog.Cancel
|
|
onclick={() => {
|
|
exceptionToDelete = undefined;
|
|
}}
|
|
>
|
|
Cancel
|
|
</AlertDialog.Cancel>
|
|
<AlertDialog.Action onclick={confirmDeleteExceptionGroup}>Delete</AlertDialog.Action>
|
|
</AlertDialog.Footer>
|
|
</AlertDialog.Content>
|
|
</AlertDialog.Root>
|
|
|
|
<!-- View Exception Modal -->
|
|
{#if viewingException}
|
|
<Modal.Root bind:open={showViewExceptionModal}>
|
|
<Modal.Content class="max-h-[90vh] max-w-2xl overflow-y-auto">
|
|
<Modal.Header>
|
|
<Modal.Title class="text-lg font-semibold">{viewingException.name}</Modal.Title>
|
|
<Modal.Description>
|
|
{viewingException.description || 'Holiday schedule details'}
|
|
</Modal.Description>
|
|
</Modal.Header>
|
|
|
|
<div class="space-y-6 px-4 pb-4">
|
|
<!-- Applied Weeks -->
|
|
<div class="space-y-2">
|
|
<h3 class="text-sm font-medium">Applied to Weeks</h3>
|
|
<div class="max-h-48 space-y-1 overflow-y-auto rounded border bg-gray-50 p-3">
|
|
{#if viewingException.weekStarts && viewingException.weekStarts.length > 0}
|
|
<div class="grid grid-cols-2 gap-2 md:grid-cols-3">
|
|
{#each viewingException.weekStarts as week (week)}
|
|
<div class="text-sm">
|
|
Week of {new SvelteDate(week).toLocaleDateString('en-GB', {
|
|
day: 'numeric',
|
|
month: 'short',
|
|
year: 'numeric'
|
|
})}
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
{:else}
|
|
<p class="text-sm text-gray-500">No weeks specified</p>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
<!-- Working Hours -->
|
|
<div class="space-y-4">
|
|
<h3 class="text-sm font-medium">Working Hours</h3>
|
|
<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">End</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{#each viewingException.hours as row (row.weekday)}
|
|
<tr class="border-t">
|
|
<td class="py-2 text-sm">{weekdayLabel(row.weekday)}</td>
|
|
<td class="py-2">
|
|
<span
|
|
class="text-sm font-medium {row.is_open
|
|
? 'text-emerald-600'
|
|
: 'text-red-600'}"
|
|
>
|
|
{row.is_open ? 'Open' : 'Closed'}
|
|
</span>
|
|
</td>
|
|
<td class="py-2 text-sm">
|
|
{row.is_open ? row.start_time : '—'}
|
|
</td>
|
|
<td class="py-2 text-sm">
|
|
{row.is_open ? row.end_time : '—'}
|
|
</td>
|
|
</tr>
|
|
{/each}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<Modal.Footer class="flex items-center justify-end gap-2">
|
|
<Button onclick={() => (showViewExceptionModal = false)}>Close</Button>
|
|
</Modal.Footer>
|
|
</Modal.Content>
|
|
</Modal.Root>
|
|
{/if}
|