feat(frontend): add done-for-day summary card to CurrentAppointment, fix schedule z-index
Replace current/next card with done-for-day summary state when all appointments are finished. Shows daily stats (payments, tips, duration, customers) and week summary when tomorrow is closed. Fix schedule grid z-index to remove stacking context that caused overlay issues. Ultraworked with Sisyphus Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -8,20 +8,28 @@
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
import { Skeleton } from '$lib/components/ui/skeleton';
|
||||
import PaymentModal from '$lib/components/payments/PaymentModal.svelte';
|
||||
import type { Booking as BookingType } from '$lib/types/booking';
|
||||
|
||||
interface Props {
|
||||
openBookingModal: (bookingId: string) => void;
|
||||
_openBookingModal: (bookingId: string) => void;
|
||||
openEditBookingModal: (bookingId: string, nextAppointmentStart?: string | null) => void;
|
||||
openUserModal: (userId: string) => void;
|
||||
}
|
||||
|
||||
let { openBookingModal, openEditBookingModal, openUserModal }: Props = $props();
|
||||
let { _openBookingModal, openEditBookingModal, openUserModal }: Props = $props();
|
||||
|
||||
type Booking = {
|
||||
id: string;
|
||||
start_time: string;
|
||||
status: string;
|
||||
notes?: string;
|
||||
user_id?: string;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
deposit_required?: boolean;
|
||||
deposit_paid?: boolean;
|
||||
amount_paid?: number;
|
||||
amount_due?: number;
|
||||
user?: {
|
||||
id: string;
|
||||
full_name: string;
|
||||
@@ -40,17 +48,60 @@
|
||||
total_amount: number;
|
||||
};
|
||||
|
||||
type DailySummary = {
|
||||
total_payments_today: number;
|
||||
total_tips_today: number;
|
||||
amount_due_today: number;
|
||||
total_duration_spent: number;
|
||||
customers_served: number;
|
||||
total_bookings: number;
|
||||
new_customers: number;
|
||||
returning_customers: number;
|
||||
guest_customers: number;
|
||||
gift_cards_sold: number;
|
||||
last_customer_name?: string;
|
||||
last_customer_visits?: number;
|
||||
summary_scope: 'day' | 'week';
|
||||
summary_start_date: string;
|
||||
summary_end_date: string;
|
||||
new_booking_services?: Array<{
|
||||
service_name: string;
|
||||
count: number;
|
||||
}>;
|
||||
};
|
||||
|
||||
let currentAppointment = $state<Booking | null>(null);
|
||||
let nextAppointment = $state<Booking | null>(null);
|
||||
let closingTime = $state<string | null>(null); // "HH:MM" format
|
||||
let freeTimeAfter = $state(0); // minutes of free time after current/next appointment
|
||||
let freeTimeCapped = $state(false); // true if free time extends to/past closing
|
||||
let closingTime = $state<string | null>(null);
|
||||
let freeTimeAfter = $state(0);
|
||||
let freeTimeCapped = $state(false);
|
||||
let loading = $state(true);
|
||||
let timeRemaining = $state(0); // minutes remaining in current appointment
|
||||
let timeRemaining = $state(0);
|
||||
let isInProgress = $state(false);
|
||||
let showPaymentModal = $state(false);
|
||||
|
||||
// Calculate time remaining and free time
|
||||
// Done-for-the-day state
|
||||
let doneForDay = $state(false);
|
||||
let summary = $state<DailySummary | null>(null);
|
||||
let weekSummary = $state<DailySummary | null>(null);
|
||||
|
||||
function formatRemainingTime(minutes: number): string {
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const mins = minutes % 60;
|
||||
if (hours === 0) return `${mins} minutes`;
|
||||
if (mins === 0) return `${hours} hour`;
|
||||
if (hours === 1) return `1 hour ${mins} minutes`;
|
||||
return `${hours} hours ${mins} minutes`;
|
||||
}
|
||||
|
||||
const minutesUntilClosing = $derived.by(() => {
|
||||
if (!closingTime) return 0;
|
||||
const [ch, cm] = closingTime.split(':').map(Number);
|
||||
const now = new SvelteDate();
|
||||
const closing = new SvelteDate(now.getFullYear(), now.getMonth(), now.getDate(), ch, cm);
|
||||
return Math.max(0, Math.floor((closing.getTime() - now.getTime()) / 60000));
|
||||
});
|
||||
|
||||
let interval: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
function calculateTimes() {
|
||||
@@ -77,7 +128,6 @@
|
||||
timeRemaining = Math.max(0, Math.floor(timeUntilMs / 60000));
|
||||
}
|
||||
|
||||
// Calculate free time after this appointment
|
||||
let rawFreeMinutes = 0;
|
||||
if (nextAppointment) {
|
||||
const nextStart = new SvelteDate(nextAppointment.start_time);
|
||||
@@ -85,7 +135,6 @@
|
||||
rawFreeMinutes = Math.max(0, Math.floor(gapMs / 60000));
|
||||
}
|
||||
|
||||
// Cap at closing time
|
||||
if (closingTime) {
|
||||
const [ch, cm] = closingTime.split(':').map(Number);
|
||||
const today = new SvelteDate();
|
||||
@@ -103,11 +152,9 @@
|
||||
);
|
||||
|
||||
if (!nextAppointment) {
|
||||
// No next appointment — show time until closing
|
||||
freeTimeAfter = minutesToClosing;
|
||||
freeTimeCapped = true;
|
||||
} else if (rawFreeMinutes > minutesToClosing) {
|
||||
// Next appointment is after closing — cap at closing
|
||||
freeTimeAfter = minutesToClosing;
|
||||
freeTimeCapped = true;
|
||||
} else {
|
||||
@@ -137,6 +184,17 @@
|
||||
currentAppointment = data.current || null;
|
||||
nextAppointment = data.next || null;
|
||||
closingTime = data.closing_time || null;
|
||||
|
||||
if (data.done_for_day) {
|
||||
doneForDay = true;
|
||||
summary = data.summary || null;
|
||||
weekSummary = data.week_summary || null;
|
||||
} else {
|
||||
doneForDay = false;
|
||||
summary = null;
|
||||
weekSummary = null;
|
||||
}
|
||||
|
||||
calculateTimes();
|
||||
} else {
|
||||
toast.error('Failed to load current appointment');
|
||||
@@ -149,20 +207,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Start interval for real-time countdown
|
||||
$effect(() => {
|
||||
fetchCurrentAndNext();
|
||||
|
||||
interval = setInterval(() => {
|
||||
calculateTimes();
|
||||
}, 15000); // Update every 15 seconds
|
||||
}, 15000);
|
||||
|
||||
return () => {
|
||||
if (interval) clearInterval(interval);
|
||||
};
|
||||
});
|
||||
|
||||
// WIP Demo actions
|
||||
function handleBegin() {
|
||||
toast.info('Begin appointment - Coming soon');
|
||||
}
|
||||
@@ -181,7 +237,7 @@
|
||||
showPaymentModal = true;
|
||||
}
|
||||
|
||||
function handlePaymentComplete(payment: unknown) {
|
||||
function handlePaymentComplete(_payment: unknown) {
|
||||
toast.success('Payment completed');
|
||||
showPaymentModal = false;
|
||||
}
|
||||
@@ -190,68 +246,104 @@
|
||||
toast.info('Cancel appointment - Coming soon');
|
||||
}
|
||||
|
||||
function isMilestone(visits: number): boolean {
|
||||
if (visits < 5) return false;
|
||||
const early = [5, 10, 15, 20, 25, 50, 75, 100, 150];
|
||||
if (visits <= 150) return early.includes(visits);
|
||||
return visits % 50 === 0;
|
||||
}
|
||||
|
||||
const activeAppointment = $derived(currentAppointment || nextAppointment);
|
||||
</script>
|
||||
|
||||
<Card.Root class="border-2 border-blue-200 bg-blue-50">
|
||||
<Card.Root
|
||||
class="border-2 {doneForDay ? 'border-teal-200 bg-teal-50' : 'border-blue-200 bg-blue-50'}"
|
||||
>
|
||||
<Card.Header>
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div class="min-w-0">
|
||||
<Card.Title class="text-xl md:text-2xl">
|
||||
{isInProgress ? 'Current Appointment' : 'Next Appointment'}
|
||||
{#if doneForDay}
|
||||
<div class="flex flex-col gap-1">
|
||||
<Card.Title class="flex items-center gap-2 text-xl md:text-2xl">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-7 w-7 text-teal-600"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
{summary?.summary_scope === 'week' ? 'Closed today' : 'All done for today!'}
|
||||
</Card.Title>
|
||||
<Card.Description class="text-base">
|
||||
{summary?.summary_scope === 'week'
|
||||
? 'Showing summary from the last working period'
|
||||
: 'No more customers booked for today'}
|
||||
</Card.Description>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div class="min-w-0">
|
||||
<Card.Title class="text-xl md:text-2xl">
|
||||
{isInProgress ? 'Current Appointment' : 'Next Appointment'}
|
||||
</Card.Title>
|
||||
{#if activeAppointment}
|
||||
<Card.Description class="text-base">
|
||||
{new SvelteDate(activeAppointment.start_time).toLocaleTimeString('en-US', {
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
hour12: true
|
||||
})}
|
||||
-
|
||||
{new SvelteDate(
|
||||
new SvelteDate(activeAppointment.start_time).getTime() +
|
||||
activeAppointment.duration_minutes * 60 * 1000
|
||||
).toLocaleTimeString('en-US', {
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
hour12: true
|
||||
})}
|
||||
</Card.Description>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if activeAppointment}
|
||||
<Card.Description class="text-base">
|
||||
{new SvelteDate(activeAppointment.start_time).toLocaleTimeString('en-US', {
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
hour12: true
|
||||
})}
|
||||
-
|
||||
{new SvelteDate(
|
||||
new SvelteDate(activeAppointment.start_time).getTime() +
|
||||
activeAppointment.duration_minutes * 60 * 1000
|
||||
).toLocaleTimeString('en-US', {
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
hour12: true
|
||||
})}
|
||||
</Card.Description>
|
||||
{#if isInProgress}
|
||||
<div class="flex flex-row flex-wrap gap-2 sm:flex-col sm:items-end">
|
||||
<Badge class="bg-blue-100 px-3 py-1 text-sm whitespace-nowrap text-blue-800">
|
||||
<span class="relative mr-2 flex h-2 w-2">
|
||||
<span
|
||||
class="absolute inline-flex h-full w-full animate-ping rounded-full bg-blue-400 opacity-75"
|
||||
></span>
|
||||
<span class="relative inline-flex h-2 w-2 rounded-full bg-blue-600"></span>
|
||||
</span>
|
||||
In Progress • {timeRemaining} min remaining
|
||||
</Badge>
|
||||
{#if timeRemaining > 29}
|
||||
{#if freeTimeCapped && freeTimeAfter === 0}
|
||||
<Badge class="bg-blue-100 px-3 py-1 text-sm whitespace-nowrap text-blue-800">
|
||||
closing after
|
||||
</Badge>
|
||||
{:else if freeTimeAfter > 0}
|
||||
<Badge class="bg-blue-100 px-3 py-1 text-sm whitespace-nowrap text-blue-800">
|
||||
{freeTimeAfter} min free afterwards{#if freeTimeCapped}
|
||||
(closing after){/if}
|
||||
</Badge>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<Badge class="bg-amber-100 px-3 py-1 text-sm whitespace-nowrap text-amber-800">
|
||||
Starts in {timeRemaining} min
|
||||
</Badge>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if activeAppointment}
|
||||
{#if isInProgress}
|
||||
<div class="flex flex-row flex-wrap gap-2 sm:flex-col sm:items-end">
|
||||
<Badge class="bg-blue-100 px-3 py-1 text-sm whitespace-nowrap text-blue-800">
|
||||
<span class="relative mr-2 flex h-2 w-2">
|
||||
<span
|
||||
class="absolute inline-flex h-full w-full animate-ping rounded-full bg-blue-400 opacity-75"
|
||||
></span>
|
||||
<span class="relative inline-flex h-2 w-2 rounded-full bg-blue-600"></span>
|
||||
</span>
|
||||
In Progress • {timeRemaining} min remaining
|
||||
</Badge>
|
||||
{#if timeRemaining > 29}
|
||||
{#if freeTimeCapped && freeTimeAfter === 0}
|
||||
<Badge class="bg-blue-100 px-3 py-1 text-sm whitespace-nowrap text-blue-800">
|
||||
closing after
|
||||
</Badge>
|
||||
{:else if freeTimeAfter > 0}
|
||||
<Badge class="bg-blue-100 px-3 py-1 text-sm whitespace-nowrap text-blue-800">
|
||||
{freeTimeAfter} min free afterwards{#if freeTimeCapped}
|
||||
(closing after){/if}
|
||||
</Badge>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<Badge class="bg-amber-100 px-3 py-1 text-sm whitespace-nowrap text-amber-800">
|
||||
Starts in {timeRemaining} min
|
||||
</Badge>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</Card.Header>
|
||||
|
||||
{#if loading}
|
||||
@@ -266,6 +358,216 @@
|
||||
</div>
|
||||
<Skeleton class="h-32 w-full" />
|
||||
</Card.Content>
|
||||
{:else if doneForDay && summary}
|
||||
<Card.Content>
|
||||
<div class="space-y-5">
|
||||
<!-- Conditional messages -->
|
||||
{#if minutesUntilClosing >= 30}
|
||||
<div class="rounded-md border border-teal-200 bg-white p-3 text-sm text-teal-800">
|
||||
There is {formatRemainingTime(minutesUntilClosing)} left to accept walk-in bookings
|
||||
</div>
|
||||
{/if}
|
||||
{#if minutesUntilClosing >= 75}
|
||||
<div class="rounded-md border border-blue-200 bg-white p-3 text-sm text-blue-800">
|
||||
Users booking online still have time to book in
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if summary.summary_scope === 'week'}
|
||||
<div class="text-xs text-gray-500">
|
||||
Summary: {new SvelteDate(summary.summary_start_date).toLocaleDateString('en-US', {
|
||||
weekday: 'short',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})}
|
||||
–
|
||||
{new SvelteDate(summary.summary_end_date).toLocaleDateString('en-US', {
|
||||
weekday: 'short',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Stats grid -->
|
||||
<div class="grid grid-cols-2 gap-3 sm:grid-cols-3">
|
||||
<div class="rounded-md border border-teal-100 bg-white p-3">
|
||||
<div class="text-xs font-medium text-gray-500">Payments Taken</div>
|
||||
<div class="mt-1 text-lg font-bold text-gray-900">
|
||||
£{summary.total_payments_today.toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border border-teal-100 bg-white p-3">
|
||||
<div class="text-xs font-medium text-gray-500">Tips</div>
|
||||
<div class="mt-1 text-lg font-bold text-gray-900">
|
||||
£{summary.total_tips_today.toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border border-teal-100 bg-white p-3">
|
||||
<div class="text-xs font-medium text-gray-500">Time in Appointments</div>
|
||||
<div class="mt-1 text-lg font-bold text-gray-900">
|
||||
{formatDuration(summary.total_duration_spent)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if summary.amount_due_today > 0}
|
||||
<div class="rounded-md border border-amber-200 bg-amber-50 p-3">
|
||||
<div class="text-xs font-medium text-amber-700">Still Due</div>
|
||||
<div class="mt-1 text-lg font-bold text-amber-900">
|
||||
£{summary.amount_due_today.toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="rounded-md border border-teal-100 bg-white p-3">
|
||||
<div class="text-xs font-medium text-gray-500">Customers Served</div>
|
||||
<div class="mt-1 text-lg font-bold text-gray-900">
|
||||
{summary.customers_served}
|
||||
{#if summary.total_bookings !== summary.customers_served}
|
||||
<span class="text-sm font-normal text-gray-500">
|
||||
across {summary.total_bookings} booking{summary.total_bookings !== 1 ? 's' : ''}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border border-teal-100 bg-white p-3">
|
||||
<div class="text-xs font-medium text-gray-500">Customers</div>
|
||||
<div class="mt-1 space-y-0.5">
|
||||
{#if summary.new_customers > 0}
|
||||
<div class="flex items-baseline justify-between gap-2">
|
||||
<span class="text-lg font-bold text-gray-900">{summary.new_customers}</span>
|
||||
<span class="text-xs text-gray-500">new</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if summary.returning_customers > 0}
|
||||
<div class="flex items-baseline justify-between gap-2">
|
||||
<span class="text-lg font-bold text-gray-900">{summary.returning_customers}</span>
|
||||
<span class="text-xs text-gray-500">returning</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if summary.guest_customers > 0}
|
||||
<div class="flex items-baseline justify-between gap-2">
|
||||
<span class="text-lg font-bold text-gray-900">{summary.guest_customers}</span>
|
||||
<span class="text-xs text-gray-500">guest</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if summary.gift_cards_sold > 0}
|
||||
<div class="rounded-md border border-teal-100 bg-white p-3">
|
||||
<div class="text-xs font-medium text-gray-500">Gift Cards Sold</div>
|
||||
<div class="mt-1 text-lg font-bold text-gray-900">{summary.gift_cards_sold}</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if summary.last_customer_name && isMilestone(summary.last_customer_visits ?? 0)}
|
||||
<div class="rounded-md border border-gray-200 bg-white p-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<div>
|
||||
<span class="font-medium text-gray-900">{summary.last_customer_name}</span>
|
||||
<span class="text-gray-700">
|
||||
has visited {summary.last_customer_visits} times now!
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if summary.new_booking_services && summary.new_booking_services.length > 0}
|
||||
<div>
|
||||
<h4 class="mb-2 text-sm font-semibold text-gray-700">
|
||||
New bookings made since{summary.summary_scope === 'week'
|
||||
? ` opening ${new SvelteDate(summary.summary_start_date).toLocaleDateString('en-US', { weekday: 'long' })}`
|
||||
: ' closing yesterday'}
|
||||
</h4>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{#each summary.new_booking_services as svc (svc.service_name)}
|
||||
<div class="rounded-md border border-gray-200 bg-white px-2.5 py-1.5 text-sm">
|
||||
<span class="font-semibold text-gray-900">{svc.count}</span>
|
||||
<span class="ml-1 text-gray-600">{svc.service_name}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Week summary (shown alongside daily summary when tomorrow is closed) -->
|
||||
{#if weekSummary}
|
||||
<hr class="border-gray-200" />
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-gray-700">This week so far</h4>
|
||||
<div class="text-xs text-gray-500">
|
||||
{new SvelteDate(weekSummary.summary_start_date).toLocaleDateString('en-US', {
|
||||
weekday: 'short',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})}
|
||||
–
|
||||
{new SvelteDate(weekSummary.summary_end_date).toLocaleDateString('en-US', {
|
||||
weekday: 'short',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-3 sm:grid-cols-3">
|
||||
<div class="rounded-md border border-gray-200 bg-white p-3">
|
||||
<div class="text-xs font-medium text-gray-500">Payments Taken</div>
|
||||
<div class="mt-1 text-lg font-bold text-gray-900">
|
||||
£{weekSummary.total_payments_today.toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border border-gray-200 bg-white p-3">
|
||||
<div class="text-xs font-medium text-gray-500">Tips</div>
|
||||
<div class="mt-1 text-lg font-bold text-gray-900">
|
||||
£{weekSummary.total_tips_today.toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if weekSummary.amount_due_today > 0}
|
||||
<div class="rounded-md border border-amber-200 bg-amber-50 p-3">
|
||||
<div class="text-xs font-medium text-amber-700">Still Due</div>
|
||||
<div class="mt-1 text-lg font-bold text-amber-900">
|
||||
£{weekSummary.amount_due_today.toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="rounded-md border border-gray-200 bg-white p-3">
|
||||
<div class="text-xs font-medium text-gray-500">Time in Appointments</div>
|
||||
<div class="mt-1 text-lg font-bold text-gray-900">
|
||||
{formatDuration(weekSummary.total_duration_spent)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border border-gray-200 bg-white p-3">
|
||||
<div class="text-xs font-medium text-gray-500">Customers Served</div>
|
||||
<div class="mt-1 text-lg font-bold text-gray-900">
|
||||
{weekSummary.customers_served}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if weekSummary.gift_cards_sold > 0}
|
||||
<div class="rounded-md border border-gray-200 bg-white p-3">
|
||||
<div class="text-xs font-medium text-gray-500">Gift Cards Sold</div>
|
||||
<div class="mt-1 text-lg font-bold text-gray-900">
|
||||
{weekSummary.gift_cards_sold}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Card.Content>
|
||||
{:else if !activeAppointment}
|
||||
<Card.Content>
|
||||
<div class="py-12 text-center">
|
||||
@@ -461,7 +763,7 @@
|
||||
{/if}
|
||||
</Card.Root>
|
||||
|
||||
{#if showPaymentModal}
|
||||
{#if showPaymentModal && activeAppointment}
|
||||
<PaymentModal
|
||||
booking={activeAppointment}
|
||||
onClose={() => (showPaymentModal = false)}
|
||||
|
||||
Reference in New Issue
Block a user