styling
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||
import { buttonVariants } from "$lib/components/ui/button/index.js";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: AlertDialogPrimitive.ActionProps = $props();
|
||||
</script>
|
||||
|
||||
<AlertDialogPrimitive.Action
|
||||
bind:ref
|
||||
data-slot="alert-dialog-action"
|
||||
class={cn(buttonVariants(), className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||
import { buttonVariants } from "$lib/components/ui/button/index.js";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: AlertDialogPrimitive.CancelProps = $props();
|
||||
</script>
|
||||
|
||||
<AlertDialogPrimitive.Cancel
|
||||
bind:ref
|
||||
data-slot="alert-dialog-cancel"
|
||||
class={cn(buttonVariants({ variant: "outline" }), className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||
import AlertDialogOverlay from "./alert-dialog-overlay.svelte";
|
||||
import { cn, type WithoutChild, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
portalProps,
|
||||
...restProps
|
||||
}: WithoutChild<AlertDialogPrimitive.ContentProps> & {
|
||||
portalProps?: WithoutChildrenOrChild<AlertDialogPrimitive.PortalProps>;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<AlertDialogPrimitive.Portal {...portalProps}>
|
||||
<AlertDialogOverlay />
|
||||
<AlertDialogPrimitive.Content
|
||||
bind:ref
|
||||
data-slot="alert-dialog-content"
|
||||
class={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed left-[50%] top-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
</AlertDialogPrimitive.Portal>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: AlertDialogPrimitive.DescriptionProps = $props();
|
||||
</script>
|
||||
|
||||
<AlertDialogPrimitive.Description
|
||||
bind:ref
|
||||
data-slot="alert-dialog-description"
|
||||
class={cn("text-muted-foreground text-sm", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="alert-dialog-footer"
|
||||
class={cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="alert-dialog-header"
|
||||
class={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: AlertDialogPrimitive.OverlayProps = $props();
|
||||
</script>
|
||||
|
||||
<AlertDialogPrimitive.Overlay
|
||||
bind:ref
|
||||
data-slot="alert-dialog-overlay"
|
||||
class={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: AlertDialogPrimitive.TitleProps = $props();
|
||||
</script>
|
||||
|
||||
<AlertDialogPrimitive.Title
|
||||
bind:ref
|
||||
data-slot="alert-dialog-title"
|
||||
class={cn("text-lg font-semibold", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: AlertDialogPrimitive.TriggerProps = $props();
|
||||
</script>
|
||||
|
||||
<AlertDialogPrimitive.Trigger bind:ref data-slot="alert-dialog-trigger" {...restProps} />
|
||||
@@ -0,0 +1,39 @@
|
||||
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||
import Trigger from "./alert-dialog-trigger.svelte";
|
||||
import Title from "./alert-dialog-title.svelte";
|
||||
import Action from "./alert-dialog-action.svelte";
|
||||
import Cancel from "./alert-dialog-cancel.svelte";
|
||||
import Footer from "./alert-dialog-footer.svelte";
|
||||
import Header from "./alert-dialog-header.svelte";
|
||||
import Overlay from "./alert-dialog-overlay.svelte";
|
||||
import Content from "./alert-dialog-content.svelte";
|
||||
import Description from "./alert-dialog-description.svelte";
|
||||
|
||||
const Root = AlertDialogPrimitive.Root;
|
||||
const Portal = AlertDialogPrimitive.Portal;
|
||||
|
||||
export {
|
||||
Root,
|
||||
Title,
|
||||
Action,
|
||||
Cancel,
|
||||
Portal,
|
||||
Footer,
|
||||
Header,
|
||||
Trigger,
|
||||
Overlay,
|
||||
Content,
|
||||
Description,
|
||||
//
|
||||
Root as AlertDialog,
|
||||
Title as AlertDialogTitle,
|
||||
Action as AlertDialogAction,
|
||||
Cancel as AlertDialogCancel,
|
||||
Portal as AlertDialogPortal,
|
||||
Footer as AlertDialogFooter,
|
||||
Header as AlertDialogHeader,
|
||||
Trigger as AlertDialogTrigger,
|
||||
Overlay as AlertDialogOverlay,
|
||||
Content as AlertDialogContent,
|
||||
Description as AlertDialogDescription,
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
@@ -13,10 +13,7 @@
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="card"
|
||||
class={cn(
|
||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||
className
|
||||
)}
|
||||
class={cn('bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
import { Textarea } from '$lib/components/ui/textarea';
|
||||
import { Separator } from '$lib/components/ui/separator';
|
||||
import * as Modal from '$lib/components/ui/dialog';
|
||||
import * as AlertDialog from '$lib/components/ui/alert-dialog';
|
||||
|
||||
// Custom component
|
||||
import FileDropZone from '$lib/components/ui/file-drop-zone.svelte';
|
||||
@@ -26,6 +27,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
// =============== Alert Dialog State ===============
|
||||
let showSaveDefaultHoursAlert = $state(false);
|
||||
let showDeleteExceptionAlert = $state(false);
|
||||
let exceptionToDelete = $state<number | undefined>(undefined);
|
||||
|
||||
// =============== Image Upload ===============
|
||||
let uploading = $state(false);
|
||||
let uploadFiles = $state<File[]>([]);
|
||||
@@ -84,7 +90,7 @@
|
||||
function formatTime(time: string): string {
|
||||
const [hours, minutes] = time.split(':').map(Number);
|
||||
|
||||
// Special case for 12:00 PM
|
||||
// Special case for 12:00
|
||||
if (hours === 12 && minutes === 0) {
|
||||
return 'Noon';
|
||||
} else if (hours === 0 && minutes === 0) {
|
||||
@@ -96,6 +102,26 @@
|
||||
return `${displayHours}:${minutes.toString().padStart(2, '0')} ${period}`;
|
||||
}
|
||||
|
||||
/** Convert formatted time back to HH:MM for input fields */
|
||||
function timeToInputValue(time: string): string {
|
||||
// Handle special cases
|
||||
if (time === 'Noon') return '12:00';
|
||||
if (time === 'Midnight') return '00:00';
|
||||
|
||||
// Parse 12-hour format
|
||||
const match = time.match(/^(\d{1,2}):(\d{2})\s*(AM|PM)$/i);
|
||||
if (!match) return time; // Return as-is if not in expected format
|
||||
|
||||
let hours = parseInt(match[1]);
|
||||
const minutes = match[2];
|
||||
const period = match[3].toUpperCase();
|
||||
|
||||
if (period === 'PM' && hours !== 12) hours += 12;
|
||||
if (period === 'AM' && hours === 12) hours = 0;
|
||||
|
||||
return `${hours.toString().padStart(2, '0')}:${minutes}`;
|
||||
}
|
||||
|
||||
async function fetchDefaultHours() {
|
||||
defaultHoursIsLoading = true;
|
||||
let error = null;
|
||||
@@ -191,8 +217,6 @@
|
||||
|
||||
onMount(loadWorkingHours);
|
||||
|
||||
let editingException = $state<ExceptionGroup | null>(null);
|
||||
let editingExceptionIndex = $state<number | null>(null);
|
||||
let showExceptionModal = $state(false);
|
||||
|
||||
const dayNames = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
||||
@@ -201,18 +225,17 @@
|
||||
function prepareDefaultHoursEdit() {
|
||||
// Deep copy the current default hours into the draft state
|
||||
defaultHoursDraft = JSON.parse(JSON.stringify(defaultHours));
|
||||
// Convert display format back to input format
|
||||
defaultHoursDraft = defaultHoursDraft.map((row) => ({
|
||||
...row,
|
||||
start_time: timeToInputValue(row.start_time),
|
||||
end_time: timeToInputValue(row.end_time)
|
||||
}));
|
||||
showDefaultHoursModal = true;
|
||||
}
|
||||
|
||||
/** Saves the default hours draft after confirmation. */
|
||||
async function saveDefaultHours() {
|
||||
if (
|
||||
!confirm(
|
||||
'Are you sure you want to save these default hours? This will affect future bookings.'
|
||||
)
|
||||
)
|
||||
return;
|
||||
|
||||
async function confirmSaveDefaultHours() {
|
||||
savingHours = true;
|
||||
const loadingToast = toast.loading('Saving default hours...');
|
||||
|
||||
@@ -238,6 +261,7 @@
|
||||
// Update the main state from the draft state if successful
|
||||
defaultHours = JSON.parse(JSON.stringify(defaultHoursDraft));
|
||||
showDefaultHoursModal = false;
|
||||
showSaveDefaultHoursAlert = false;
|
||||
toast.success('Default hours saved successfully!', { id: loadingToast });
|
||||
} else if (response.status === 401 || response.status === 403) {
|
||||
toast.error('Unauthorized. Please log in again.', { id: loadingToast });
|
||||
@@ -254,57 +278,21 @@
|
||||
}
|
||||
|
||||
async function saveExceptionGroup() {
|
||||
// Simplified save logic for demo
|
||||
if (!editingException) return;
|
||||
|
||||
savingHours = true;
|
||||
try {
|
||||
await new Promise((r) => setTimeout(r, 1000));
|
||||
|
||||
if (editingExceptionIndex === null) {
|
||||
// Mock ID assignment for new group
|
||||
editingException.id = Math.max(0, ...exceptionGroups.map((g) => g.id ?? 0)) + 1;
|
||||
exceptionGroups = [editingException, ...exceptionGroups];
|
||||
} else {
|
||||
// Update existing group
|
||||
exceptionGroups = exceptionGroups.map((g, i) =>
|
||||
i === editingExceptionIndex ? editingException! : g
|
||||
);
|
||||
// TODO
|
||||
}
|
||||
|
||||
showExceptionModal = false;
|
||||
editingException = null;
|
||||
} catch (err) {
|
||||
console.error('save exception', err);
|
||||
}
|
||||
savingHours = false;
|
||||
}
|
||||
|
||||
async function deleteExceptionGroup(id?: number) {
|
||||
if (!id) return;
|
||||
if (!confirm('Delete this exception group? This will remove its rows too.')) return;
|
||||
|
||||
async function confirmDeleteExceptionGroup() {
|
||||
try {
|
||||
await new Promise((r) => setTimeout(r, 500));
|
||||
exceptionGroups = exceptionGroups.filter((g) => g.id !== id);
|
||||
exceptionGroups = exceptionGroups.filter((g) => g.id !== exceptionToDelete);
|
||||
showDeleteExceptionAlert = false;
|
||||
exceptionToDelete = undefined;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
function createNewException() {
|
||||
editingExceptionIndex = null;
|
||||
editingException = {
|
||||
name: '',
|
||||
description: '',
|
||||
week_starts: [],
|
||||
rows: Array.from({ length: 7 }, (_, i) => ({
|
||||
weekday: i,
|
||||
start_time: '09:00',
|
||||
end_time: '17:00',
|
||||
is_open: true
|
||||
}))
|
||||
};
|
||||
showExceptionModal = true;
|
||||
}
|
||||
|
||||
@@ -621,18 +609,19 @@
|
||||
|
||||
<Card.Content class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<Button variant="outline" onclick={createNewException}>New schedule</Button>
|
||||
<Button variant="default" onclick={createNewException}>New schedule</Button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div class="grid grid-cols-1 gap-4 space-y-3 md:grid-cols-2">
|
||||
{#if exceptionGroups.length === 0}
|
||||
<p class="text-sm text-gray-500">No exception groups found.</p>
|
||||
{/if}
|
||||
|
||||
{#each exceptionGroups as g}
|
||||
<div class="rounded border p-3">
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<div class="relative h-full rounded border p-3 pb-12">
|
||||
<!-- add bottom padding to avoid overlap -->
|
||||
<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">
|
||||
@@ -643,18 +632,25 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- Absolute positioned button -->
|
||||
<div class="absolute bottom-3 right-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
variant="default"
|
||||
class="w-20"
|
||||
onclick={() => {
|
||||
editingExceptionIndex = exceptionGroups.indexOf(g);
|
||||
editingException = JSON.parse(JSON.stringify(g));
|
||||
showExceptionModal = true;
|
||||
alert('TODO - view group');
|
||||
}}
|
||||
>
|
||||
Edit
|
||||
View
|
||||
</Button>
|
||||
<Button variant="destructive" onclick={() => deleteExceptionGroup(g.id)}>
|
||||
<Button
|
||||
variant="destructive"
|
||||
class="w-20"
|
||||
onclick={() => {
|
||||
exceptionToDelete = g.id;
|
||||
showDeleteExceptionAlert = true;
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
@@ -744,6 +740,7 @@
|
||||
</Card.Root>
|
||||
</div>
|
||||
|
||||
<!-- Default Hours Modal -->
|
||||
<Modal.Root bind:open={showDefaultHoursModal}>
|
||||
<Modal.Content class="max-h-[90vh] max-w-sm overflow-y-auto md:max-w-lg">
|
||||
<Modal.Header>
|
||||
@@ -807,119 +804,40 @@
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onclick={saveDefaultHours} disabled={savingHours}>
|
||||
{savingHours ? 'Saving…' : 'Save Defaults'}
|
||||
<Button onclick={() => (showSaveDefaultHoursAlert = true)} disabled={savingHours}>
|
||||
Save Defaults
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal.Content>
|
||||
</Modal.Root>
|
||||
|
||||
<!-- Save Default Hours Confirmation -->
|
||||
<AlertDialog.Root bind:open={showSaveDefaultHoursAlert}>
|
||||
<AlertDialog.Content class="z-[60]">
|
||||
<AlertDialog.Header>
|
||||
<AlertDialog.Title>Save default hours?</AlertDialog.Title>
|
||||
<AlertDialog.Description>
|
||||
Are you sure you want to save these default hours? This will affect future bookings.
|
||||
</AlertDialog.Description>
|
||||
</AlertDialog.Header>
|
||||
<AlertDialog.Footer>
|
||||
<AlertDialog.Cancel>Cancel</AlertDialog.Cancel>
|
||||
<AlertDialog.Action onclick={confirmSaveDefaultHours}>Continue</AlertDialog.Action>
|
||||
</AlertDialog.Footer>
|
||||
</AlertDialog.Content>
|
||||
</AlertDialog.Root>
|
||||
|
||||
<!-- Exception Group Modal -->
|
||||
<Modal.Root bind:open={showExceptionModal}>
|
||||
<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.Title class="text-lg font-semibold">New Exception Group</Modal.Title>
|
||||
</Modal.Header>
|
||||
|
||||
{#if editingException}
|
||||
<div class="space-y-4 pb-4">
|
||||
<div>
|
||||
<Label>Name</Label>
|
||||
<Input bind:value={editingException.name} placeholder="e.g. Christmas" />
|
||||
</div>
|
||||
<div>
|
||||
<Label>Description</Label>
|
||||
<Textarea
|
||||
rows={3}
|
||||
bind:value={editingException.description}
|
||||
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 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;
|
||||
if (fromEl?.value && toEl?.value) {
|
||||
addWeeksToException(fromEl.value, toEl.value, editingException!.week_starts);
|
||||
editingException!.week_starts = Array.from(
|
||||
new Set(editingException!.week_starts).values()
|
||||
).sort();
|
||||
}
|
||||
}}
|
||||
>
|
||||
Add weeks
|
||||
</Button>
|
||||
</div>
|
||||
<div class="mt-2 text-xs text-gray-500">
|
||||
Weeks are added by their starting Monday (ISO date).
|
||||
</div>
|
||||
|
||||
<div class="mt-2">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{#each editingException.week_starts as ws}
|
||||
<div class="flex items-center gap-2 rounded bg-gray-100 px-2 py-1 text-xs">
|
||||
<span>{ws}</span>
|
||||
<button
|
||||
class="text-red-500"
|
||||
onclick={() =>
|
||||
(editingException!.week_starts = editingException!.week_starts.filter(
|
||||
(w) => w !== ws
|
||||
))}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div>
|
||||
<div class="mb-2 text-sm font-medium">Weekday rows (Override)</div>
|
||||
<div class="grid gap-2">
|
||||
{#each editingException.rows as r}
|
||||
<div class="flex items-center gap-2">
|
||||
<div style="width:48px" class="text-sm">{weekdayLabel(r.weekday)}</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={r.is_open}
|
||||
class="text-primary focus:ring-primary h-4 w-4 rounded border-gray-300 bg-gray-100"
|
||||
/>
|
||||
<Input
|
||||
type="time"
|
||||
bind:value={r.start_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-[70px] text-sm"
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Modal.Footer class="flex items-center justify-end gap-2 p-0 pt-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
onclick={() => {
|
||||
showExceptionModal = false;
|
||||
editingException = null;
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
@@ -931,6 +849,30 @@
|
||||
</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>
|
||||
|
||||
<!-- User Modal -->
|
||||
{#if selectedUser}
|
||||
<Modal.Root bind:open={showUserModal}>
|
||||
<Modal.Content class="max-h-[90vh] max-w-sm overflow-y-auto md:max-w-lg">
|
||||
@@ -988,24 +930,12 @@
|
||||
|
||||
<Modal.Footer class="flex items-center justify-end gap-2">
|
||||
<Button onclick={() => (showUserModal = false)}>Close</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onclick={async () => {
|
||||
if (!confirm('Delete user? This is irreversible.')) return;
|
||||
// Mock delete
|
||||
await new Promise((r) => setTimeout(r, 500));
|
||||
users = users.filter((u) => u.id !== selectedUser!.id);
|
||||
showUserModal = false;
|
||||
alert(`User ${selectedUser!.fn || selectedUser!.id} deleted (mocked)`);
|
||||
}}
|
||||
>
|
||||
Delete user
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal.Content>
|
||||
</Modal.Root>
|
||||
{/if}
|
||||
|
||||
<!-- Booking Modal -->
|
||||
{#if selectedBooking}
|
||||
<Modal.Root bind:open={showBookingModal}>
|
||||
<Modal.Content class="max-h-[90vh] max-w-sm overflow-y-auto md:max-w-lg">
|
||||
|
||||
Reference in New Issue
Block a user