WIP call in booking
This commit is contained in:
@@ -168,57 +168,193 @@ func AdminEditBookingHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.WriteHeader(http.StatusNoContent)
|
w.WriteHeader(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AdminCreateBookingForUserHandler creates a booking on behalf of a user.
|
type AdminCreateBookingForUserRequest struct {
|
||||||
func AdminCreateBookingForUserHandler(w http.ResponseWriter, r *http.Request) {
|
UserID string `json:"user_id" validate:"required"`
|
||||||
userID := chi.URLParam(r, "user_id")
|
StartTime time.Time `json:"start_time" validate:"required"`
|
||||||
var req CreateBookingRequest
|
ServiceIDs []string `json:"service_ids" validate:"required,min=1"`
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
ServiceOverrides []ServiceOverride `json:"service_overrides,omitempty"`
|
||||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
Notes *string `json:"notes,omitempty"` // staff notes
|
||||||
|
}
|
||||||
|
|
||||||
|
func oo(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Admin identity (creator)
|
||||||
|
adminID, ok := r.Context().Value(mw.UserIDKey).(string)
|
||||||
|
if !ok || adminID == "" {
|
||||||
|
http.Error(w, "Authentication required", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
adminID, _ := r.Context().Value(mw.UserIDKey).(string)
|
var req AdminCreateBookingForUserRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
log.Printf("Failed to decode request: %v", err)
|
||||||
|
http.Error(w, "Invalid request", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic validation
|
||||||
|
if req.UserID == "" {
|
||||||
|
http.Error(w, "User ID is required", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if req.StartTime.IsZero() {
|
||||||
|
http.Error(w, "Start time is required", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(req.ServiceIDs) == 0 {
|
||||||
|
http.Error(w, "At least one service is required", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate overrides
|
||||||
|
for _, override := range req.ServiceOverrides {
|
||||||
|
if override.ServiceID == "" {
|
||||||
|
http.Error(w, "Service ID is required for overrides", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if override.OverridePrice != nil && *override.OverridePrice < 0 {
|
||||||
|
http.Error(w, "Override price cannot be negative", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if override.OverrideDurationMinutes != nil && *override.OverrideDurationMinutes <= 0 {
|
||||||
|
http.Error(w, "Override duration must be positive", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tx, err := db.DB.Begin(r.Context())
|
tx, err := db.DB.Begin(r.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to begin transaction: %v", err)
|
log.Printf("Failed to start transaction: %v", err)
|
||||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer tx.Rollback(r.Context())
|
defer tx.Rollback(r.Context())
|
||||||
|
|
||||||
var bookingID string
|
// Create booking directly as confirmed
|
||||||
err = tx.QueryRow(r.Context(), `
|
bookingQuery := `
|
||||||
INSERT INTO bookings (id, user_id, start_time, status, created_by)
|
INSERT INTO bookings (
|
||||||
VALUES (generate_booking_id(), $1, $2, 'pending', $3)
|
user_id,
|
||||||
RETURNING id
|
start_time,
|
||||||
`, userID, req.StartTime, adminID).Scan(&bookingID)
|
status,
|
||||||
|
notes,
|
||||||
|
created_by
|
||||||
|
)
|
||||||
|
VALUES ($1, $2, 'confirmed', $3, $4)
|
||||||
|
RETURNING id, user_id, start_time, status, notes, created_at, updated_at, created_by
|
||||||
|
`
|
||||||
|
|
||||||
|
var booking Booking
|
||||||
|
booking.User = &UserSummary{}
|
||||||
|
|
||||||
|
err = tx.QueryRow(
|
||||||
|
r.Context(),
|
||||||
|
bookingQuery,
|
||||||
|
req.UserID,
|
||||||
|
req.StartTime,
|
||||||
|
req.Notes,
|
||||||
|
adminID,
|
||||||
|
).Scan(
|
||||||
|
&booking.ID,
|
||||||
|
&booking.User.ID,
|
||||||
|
&booking.StartTime,
|
||||||
|
&booking.Status,
|
||||||
|
&booking.Notes,
|
||||||
|
&booking.CreatedAt,
|
||||||
|
&booking.UpdatedAt,
|
||||||
|
&booking.CreatedBy,
|
||||||
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to create booking: %v", err)
|
log.Printf("Failed to create admin booking: %v", err)
|
||||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, svcID := range req.ServiceIDs {
|
// Insert booking services
|
||||||
if _, err := tx.Exec(r.Context(), `
|
serviceInsertQuery := `
|
||||||
INSERT INTO booking_services (booking_id, service_id)
|
INSERT INTO booking_services (booking_id, service_id)
|
||||||
VALUES ($1, $2)
|
VALUES ($1, $2)
|
||||||
`, bookingID, svcID); err != nil {
|
`
|
||||||
log.Printf("Failed to insert booking service: %v", err)
|
|
||||||
|
for _, serviceID := range req.ServiceIDs {
|
||||||
|
_, err := tx.Exec(r.Context(), serviceInsertQuery, booking.ID, serviceID)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to insert booking service %s: %v", serviceID, err)
|
||||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = tx.Commit(r.Context()); err != nil {
|
// Apply overrides (optional)
|
||||||
log.Printf("Failed to commit transaction: %v", err)
|
if len(req.ServiceOverrides) > 0 {
|
||||||
|
// Ensure overrides only reference services in this booking
|
||||||
|
serviceCheckQuery := `
|
||||||
|
SELECT COUNT(*) FROM booking_services
|
||||||
|
WHERE booking_id = $1 AND service_id = ANY($2)
|
||||||
|
`
|
||||||
|
|
||||||
|
overrideServiceIDs := make([]string, len(req.ServiceOverrides))
|
||||||
|
for i, o := range req.ServiceOverrides {
|
||||||
|
overrideServiceIDs[i] = o.ServiceID
|
||||||
|
}
|
||||||
|
|
||||||
|
var count int
|
||||||
|
err = tx.QueryRow(
|
||||||
|
r.Context(),
|
||||||
|
serviceCheckQuery,
|
||||||
|
booking.ID,
|
||||||
|
overrideServiceIDs,
|
||||||
|
).Scan(&count)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to verify service overrides: %v", err)
|
||||||
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if count != len(req.ServiceOverrides) {
|
||||||
|
http.Error(w, "One or more service overrides do not belong to this booking", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
overrideUpdateQuery := `
|
||||||
|
UPDATE booking_services
|
||||||
|
SET override_price = $1,
|
||||||
|
override_duration_minutes = $2
|
||||||
|
WHERE booking_id = $3 AND service_id = $4
|
||||||
|
`
|
||||||
|
|
||||||
|
for _, override := range req.ServiceOverrides {
|
||||||
|
_, err := tx.Exec(
|
||||||
|
r.Context(),
|
||||||
|
overrideUpdateQuery,
|
||||||
|
override.OverridePrice,
|
||||||
|
override.OverrideDurationMinutes,
|
||||||
|
booking.ID,
|
||||||
|
override.ServiceID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf(
|
||||||
|
"Failed to apply override (booking %s, service %s): %v",
|
||||||
|
booking.ID,
|
||||||
|
override.ServiceID,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Commit(r.Context()); err != nil {
|
||||||
|
log.Printf("Failed to commit admin booking creation: %v", err)
|
||||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.WriteHeader(http.StatusCreated)
|
w.WriteHeader(http.StatusCreated)
|
||||||
if err := json.NewEncoder(w).Encode(map[string]string{"id": bookingID}); err != nil {
|
if err := json.NewEncoder(w).Encode(booking); err != nil {
|
||||||
log.Printf("Failed to encode response: %v", err)
|
log.Printf("Failed to encode response: %v", err)
|
||||||
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -128,12 +128,13 @@ func main() {
|
|||||||
|
|
||||||
r.Route("/admin/bookings", func(r chi.Router) {
|
r.Route("/admin/bookings", func(r chi.Router) {
|
||||||
r.Get("/", bookings.GetAllAdminBookingsHandler)
|
r.Get("/", bookings.GetAllAdminBookingsHandler)
|
||||||
|
r.Post("/", bookings.AdminCreateBookingForUserHandler)
|
||||||
r.Get("/search", bookings.SearchAdminBookingsHandler)
|
r.Get("/search", bookings.SearchAdminBookingsHandler)
|
||||||
r.Get("/user/{user_id}", bookings.GetAllBookingsByUserHandler)
|
r.Get("/user/{user_id}", bookings.GetAllBookingsByUserHandler)
|
||||||
r.Get("/{id}", bookings.GetAdminBookingHandler)
|
r.Get("/{id}", bookings.GetAdminBookingHandler)
|
||||||
r.Put("/{id}/progress", bookings.ProgressBookingHandler)
|
r.Put("/{id}/progress", bookings.ProgressBookingHandler)
|
||||||
r.Post("/{id}/confirm", bookings.ConfirmBookingHandler)
|
r.Post("/{id}/confirm", bookings.ConfirmBookingHandler)
|
||||||
r.Post("/{id}/cancel", bookings.ConfirmBookingHandler)
|
r.Post("/{id}/cancel", bookings.ConfirmBookingHandler) // todo
|
||||||
})
|
})
|
||||||
|
|
||||||
r.Route("/admin/users", func(r chi.Router) {
|
r.Route("/admin/users", func(r chi.Router) {
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { authStore } from '$lib/stores/auth.svelte';
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
import * as Modal from '$lib/components/ui/dialog';
|
||||||
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
import { Input } from '$lib/components/ui/input';
|
||||||
|
import { Textarea } from '$lib/components/ui/textarea';
|
||||||
|
import { SvelteDate } from 'svelte/reactivity';
|
||||||
|
|
||||||
|
export let open = false;
|
||||||
|
export let initialUserId: string | null = null;
|
||||||
|
|
||||||
|
let userId = initialUserId ?? '';
|
||||||
|
let startTime = new SvelteDate().toISOString().slice(0, 16); // datetime-local
|
||||||
|
let serviceIds: string[] = [];
|
||||||
|
let notes = '';
|
||||||
|
|
||||||
|
// You likely already have this elsewhere
|
||||||
|
let services: Array<{ id: string; name: string }> = [];
|
||||||
|
|
||||||
|
async function loadServices() {
|
||||||
|
const res = await fetch('/api/services');
|
||||||
|
if (res.ok) services = await res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submit() {
|
||||||
|
if (!userId || serviceIds.length === 0) {
|
||||||
|
toast.error('User and at least one service are required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch('/api/admin/bookings', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${authStore.currentToken}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
user_id: userId,
|
||||||
|
start_time: new Date(startTime).toISOString(),
|
||||||
|
service_ids: serviceIds,
|
||||||
|
notes
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const text = await res.text();
|
||||||
|
toast.error(text || 'Failed to create booking');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success('Booking created and confirmed');
|
||||||
|
open = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$: if (open) loadServices();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal.Root bind:open>
|
||||||
|
<Modal.Content class="max-w-lg">
|
||||||
|
<Modal.Header>
|
||||||
|
<Modal.Title>Create Booking</Modal.Title>
|
||||||
|
</Modal.Header>
|
||||||
|
|
||||||
|
<div class="space-y-4 p-4">
|
||||||
|
<div>
|
||||||
|
<label class="text-xs text-gray-500">User ID</label>
|
||||||
|
<Input bind:value={userId} placeholder="Paste or search user ID" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="text-xs text-gray-500">Start Time</label>
|
||||||
|
<Input type="datetime-local" bind:value={startTime} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="text-xs text-gray-500">Services</label>
|
||||||
|
<div class="space-y-2">
|
||||||
|
{#each services as service}
|
||||||
|
<label class="flex items-center gap-2 text-sm">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
value={service.id}
|
||||||
|
onchange={(e) => {
|
||||||
|
const checked = e.currentTarget.checked;
|
||||||
|
serviceIds = checked
|
||||||
|
? [...serviceIds, service.id]
|
||||||
|
: serviceIds.filter((id) => id !== service.id);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{service.name}
|
||||||
|
</label>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="text-xs text-gray-500">Staff Notes</label>
|
||||||
|
<Textarea rows={3} bind:value={notes} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Modal.Footer class="flex justify-end gap-2">
|
||||||
|
<Button variant="outline" onclick={() => (open = false)}>Cancel</Button>
|
||||||
|
<Button onclick={submit}>Create & Confirm</Button>
|
||||||
|
</Modal.Footer>
|
||||||
|
</Modal.Content>
|
||||||
|
</Modal.Root>
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { authStore } from '$lib/stores/auth.svelte';
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
import { Input } from '$lib/components/ui/input';
|
||||||
|
import { Textarea } from '$lib/components/ui/textarea';
|
||||||
|
import { SvelteDate } from 'svelte/reactivity';
|
||||||
|
import BookingCreateModal from '$lib/components/admin/BookingCreateModal.svelte';
|
||||||
|
|
||||||
|
let showCreateModal = false;
|
||||||
|
let selectedUserId: string | null = null;
|
||||||
|
|
||||||
|
function openForUser(userId: string) {
|
||||||
|
selectedUserId = userId;
|
||||||
|
showCreateModal = true;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="rounded-lg border border-gray-200 bg-white p-4">
|
||||||
|
<h3 class="mb-2 text-sm font-semibold tracking-wide text-gray-600 uppercase">
|
||||||
|
Call-In / Walk-In Booking
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<p class="mb-4 text-sm text-gray-500">
|
||||||
|
Create and confirm a booking immediately while speaking with the client.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<Button onclick={() => (showCreateModal = true)}>Create Booking</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if showCreateModal}
|
||||||
|
<BookingCreateModal bind:open={showCreateModal} initialUserId={selectedUserId} />
|
||||||
|
{/if}
|
||||||
@@ -24,6 +24,12 @@
|
|||||||
duration_minutes: number;
|
duration_minutes: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function isPastAppointment(startTime: string, durationMinutes: number): boolean {
|
||||||
|
const start = new Date(startTime);
|
||||||
|
const end = new Date(start.getTime() + durationMinutes * 60_000);
|
||||||
|
return end.getTime() < Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
let appointments = $state<TodayAppointment[]>([]);
|
let appointments = $state<TodayAppointment[]>([]);
|
||||||
let loading = $state(true);
|
let loading = $state(true);
|
||||||
|
|
||||||
@@ -179,7 +185,8 @@
|
|||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
{#each appointments as apt (apt.id)}
|
{#each appointments as apt (apt.id)}
|
||||||
<div
|
<div
|
||||||
class="flex items-center gap-4 rounded-lg border p-3 transition-all hover:shadow-md"
|
class="flex items-center gap-4 rounded-lg border p-3 transition-all hover:shadow-md
|
||||||
|
{isPastAppointment(apt.start_time, apt.duration_minutes) ? 'line-through opacity-50' : ''}"
|
||||||
>
|
>
|
||||||
<div class="min-w-[80px] text-sm font-semibold text-gray-700">
|
<div class="min-w-[80px] text-sm font-semibold text-gray-700">
|
||||||
{formatTime(apt.start_time)}
|
{formatTime(apt.start_time)}
|
||||||
|
|||||||
@@ -9,11 +9,9 @@
|
|||||||
import CurrentAppointment from '$lib/components/today/CurrentAppointment.svelte';
|
import CurrentAppointment from '$lib/components/today/CurrentAppointment.svelte';
|
||||||
import TodayCalendar from '$lib/components/today/TodayCalendar.svelte';
|
import TodayCalendar from '$lib/components/today/TodayCalendar.svelte';
|
||||||
import PendingApprovals from '$lib/components/today/PendingApprovals.svelte';
|
import PendingApprovals from '$lib/components/today/PendingApprovals.svelte';
|
||||||
// import QuickStats from '$lib/components/today/QuickStats.svelte';
|
|
||||||
// import UpcomingArrivals from '$lib/components/today/UpcomingArrivals.svelte';
|
|
||||||
// import RecentCheckouts from '$lib/components/today/RecentCheckouts.svelte';
|
|
||||||
// import TodayRevenue from '$lib/components/today/TodayRevenue.svelte';
|
// import TodayRevenue from '$lib/components/today/TodayRevenue.svelte';
|
||||||
// import LoyaltyRedemptions from '$lib/components/today/LoyaltyRedemptions.svelte';
|
// import LoyaltyStats from '$lib/components/today/LoyaltyStats.svelte';
|
||||||
|
import CallInBooking from '$lib/components/admin/CallInBooking.svelte';
|
||||||
import BookingModal from '$lib/components/admin/BookingModal.svelte';
|
import BookingModal from '$lib/components/admin/BookingModal.svelte';
|
||||||
import UserModal from '$lib/components/admin/UserModal.svelte';
|
import UserModal from '$lib/components/admin/UserModal.svelte';
|
||||||
|
|
||||||
@@ -109,6 +107,19 @@
|
|||||||
<!-- Current/Next Appointment Card (Full Width) -->
|
<!-- Current/Next Appointment Card (Full Width) -->
|
||||||
<CurrentAppointment {openBookingModal} {openUserModal} />
|
<CurrentAppointment {openBookingModal} {openUserModal} />
|
||||||
|
|
||||||
|
<!-- quick booking Grid -->
|
||||||
|
<div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
||||||
|
<!-- Left Column: Create a booking for a call in or messaging user (2/3 width on large screens) -->
|
||||||
|
<div class="lg:col-span-2">
|
||||||
|
<CallInBooking />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right Column: Similar to the above but immediately block out my next availible working space while I talk over the walk-in users needs and chat etc (1/3 width on large screens) -->
|
||||||
|
<div class="space-y-6">
|
||||||
|
<!-- <WalkInBooking /> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Main Content Grid -->
|
<!-- Main Content Grid -->
|
||||||
<div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
<div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
||||||
<!-- Left Column: Today's Calendar (2/3 width on large screens) -->
|
<!-- Left Column: Today's Calendar (2/3 width on large screens) -->
|
||||||
@@ -120,25 +131,16 @@
|
|||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<!-- 3 oldest pending appointments for approval -->
|
<!-- 3 oldest pending appointments for approval -->
|
||||||
<PendingApprovals {openBookingModal} />
|
<PendingApprovals {openBookingModal} />
|
||||||
|
|
||||||
<!-- Quick Stats -->
|
|
||||||
<!-- <QuickStats /> -->
|
|
||||||
|
|
||||||
<!-- Upcoming Arrivals (Next 2 hours) -->
|
|
||||||
<!-- <UpcomingArrivals {openBookingModal} {openUserModal} /> -->
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Bottom Row: Additional Panels -->
|
<!-- Bottom Row: Additional Panels -->
|
||||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
|
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||||
<!-- Recent Checkouts -->
|
|
||||||
<!-- <RecentCheckouts {openBookingModal} /> -->
|
|
||||||
|
|
||||||
<!-- Today's Revenue -->
|
<!-- Today's Revenue -->
|
||||||
<!-- <TodayRevenue /> -->
|
<!-- <TodayRevenue /> -->
|
||||||
|
|
||||||
<!-- Loyalty Redemptions Today -->
|
<!-- Loyalty point accumulations and redemptions today -->
|
||||||
<!-- <LoyaltyRedemptions {openUserModal} /> -->
|
<!-- <LoyaltyStats {openUserModal} /> -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user