Compare commits
No commits in common. "cc2adcb808ea7efcf43d9174dec8dbfa0bd14aea" and "6a0f1c820a5576fa0e22d3e31a97047f3078243d" have entirely different histories.
cc2adcb808
...
6a0f1c820a
5 changed files with 5 additions and 270 deletions
|
|
@ -28,7 +28,6 @@ import {
|
||||||
import { useAuth } from '@/composables/useAuthService'
|
import { useAuth } from '@/composables/useAuthService'
|
||||||
import { formatSats } from '@/lib/utils/formatting'
|
import { formatSats } from '@/lib/utils/formatting'
|
||||||
import { useBookingFlow } from '../composables/useBookingFlow'
|
import { useBookingFlow } from '../composables/useBookingFlow'
|
||||||
import type { PendingBooking } from '../composables/usePendingBooking'
|
|
||||||
import type { AvailabilityResult, Room } from '../types/room'
|
import type { AvailabilityResult, Room } from '../types/room'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
@ -36,11 +35,6 @@ interface Props {
|
||||||
/** The availability quote the guest just saw — dates + nights + price. */
|
/** The availability quote the guest just saw — dates + nights + price. */
|
||||||
availability: AvailabilityResult
|
availability: AvailabilityResult
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
/**
|
|
||||||
* When set, open straight to the invoice for a held booking saved on this
|
|
||||||
* device (the guest closed the dialog earlier) instead of the guest form.
|
|
||||||
*/
|
|
||||||
resume?: PendingBooking | null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Emits {
|
interface Emits {
|
||||||
|
|
@ -167,19 +161,11 @@ watch(
|
||||||
if (open) {
|
if (open) {
|
||||||
flow.reset()
|
flow.reset()
|
||||||
confirmClose.value = false
|
confirmClose.value = false
|
||||||
if (props.resume) {
|
form.resetForm({ values: { num_guests: 1, guest_contact: '', message: '' } })
|
||||||
// Skip the guest form: rehydrate the saved invoice and poll.
|
|
||||||
void flow.resume(props.resume)
|
|
||||||
} else {
|
|
||||||
form.resetForm({ values: { num_guests: 1, guest_contact: '', message: '' } })
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
flow.reset()
|
flow.reset()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// The dialog is v-if-mounted with isOpen already true, so fire on mount —
|
|
||||||
// otherwise a resume never triggers flow.resume() and lands on the form.
|
|
||||||
{ immediate: true },
|
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,7 @@ import { toast } from 'vue-sonner'
|
||||||
import { SERVICE_TOKENS, tryInjectService } from '@/core/di-container'
|
import { SERVICE_TOKENS, tryInjectService } from '@/core/di-container'
|
||||||
import type { PaymentService } from '@/core/services/PaymentService'
|
import type { PaymentService } from '@/core/services/PaymentService'
|
||||||
import type { ChateletApiService } from '../services/ChateletApiService'
|
import type { ChateletApiService } from '../services/ChateletApiService'
|
||||||
import { LOST_BOOKING_STATUSES, type Booking, type BookingQuote, type BookingRequest, type BookingStatus } from '../types/booking'
|
import { LOST_BOOKING_STATUSES, type BookingQuote, type BookingRequest, type BookingStatus } from '../types/booking'
|
||||||
import { usePendingBooking, type PendingBooking } from './usePendingBooking'
|
|
||||||
|
|
||||||
const POLL_MS = 2000
|
const POLL_MS = 2000
|
||||||
|
|
||||||
|
|
@ -19,7 +18,6 @@ export function useBookingFlow() {
|
||||||
// DI, never a direct import (workspace rule).
|
// DI, never a direct import (workspace rule).
|
||||||
const api = tryInjectService<ChateletApiService>(SERVICE_TOKENS.CHATELET_API)
|
const api = tryInjectService<ChateletApiService>(SERVICE_TOKENS.CHATELET_API)
|
||||||
const paymentService = tryInjectService<PaymentService>(SERVICE_TOKENS.PAYMENT_SERVICE)
|
const paymentService = tryInjectService<PaymentService>(SERVICE_TOKENS.PAYMENT_SERVICE)
|
||||||
const pending = usePendingBooking()
|
|
||||||
|
|
||||||
const quote = ref<BookingQuote | null>(null)
|
const quote = ref<BookingQuote | null>(null)
|
||||||
const qrCode = ref<string | null>(null)
|
const qrCode = ref<string | null>(null)
|
||||||
|
|
@ -46,9 +44,6 @@ export function useBookingFlow() {
|
||||||
try {
|
try {
|
||||||
const q = await api.requestBooking(data)
|
const q = await api.requestBooking(data)
|
||||||
quote.value = q
|
quote.value = q
|
||||||
// Persist the invoice so closing the dialog doesn't strand the hold:
|
|
||||||
// the guest can return and pay it (the public read-back has no bolt11).
|
|
||||||
pending.save(q)
|
|
||||||
try {
|
try {
|
||||||
qrCode.value = paymentService ? await paymentService.generateQRCode(q.payment_request) : null
|
qrCode.value = paymentService ? await paymentService.generateQRCode(q.payment_request) : null
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -61,42 +56,6 @@ export function useBookingFlow() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Re-open a held booking saved on this device (guest closed the dialog
|
|
||||||
* earlier). Rehydrates the invoice from local storage — no server round
|
|
||||||
* trip for the bolt11, which the public endpoint never returns — and
|
|
||||||
* resumes the settlement poll, which will surface a confirmed or lapsed
|
|
||||||
* hold on its first tick.
|
|
||||||
*/
|
|
||||||
async function resume(pb: PendingBooking): Promise<void> {
|
|
||||||
const booking: Booking = {
|
|
||||||
id: pb.bookingId,
|
|
||||||
room_id: pb.roomId,
|
|
||||||
guest_pubkey: '',
|
|
||||||
guest_contact: null,
|
|
||||||
check_in: pb.checkIn,
|
|
||||||
check_out: pb.checkOut,
|
|
||||||
nights: pb.nights,
|
|
||||||
num_guests: pb.numGuests,
|
|
||||||
currency: pb.currency ?? '',
|
|
||||||
price_fiat: pb.quoteFiat ?? 0,
|
|
||||||
amount_sat: pb.amountSat,
|
|
||||||
deposit_sat: pb.depositSat,
|
|
||||||
status: 'awaiting_payment',
|
|
||||||
payment_hash: pb.paymentHash,
|
|
||||||
expires_at: pb.expiresAt,
|
|
||||||
created_at: pb.savedAt,
|
|
||||||
updated_at: pb.savedAt,
|
|
||||||
}
|
|
||||||
quote.value = { booking, payment_request: pb.paymentRequest, payment_hash: pb.paymentHash }
|
|
||||||
try {
|
|
||||||
qrCode.value = paymentService ? await paymentService.generateQRCode(pb.paymentRequest) : null
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error generating QR code:', err)
|
|
||||||
}
|
|
||||||
startPolling(pb.bookingId)
|
|
||||||
}
|
|
||||||
|
|
||||||
function startPolling(bookingId: string) {
|
function startPolling(bookingId: string) {
|
||||||
stopPolling()
|
stopPolling()
|
||||||
isPaymentPending.value = true
|
isPaymentPending.value = true
|
||||||
|
|
@ -108,11 +67,9 @@ export function useBookingFlow() {
|
||||||
if (quote.value) quote.value = { ...quote.value, booking: { ...quote.value.booking, ...b } }
|
if (quote.value) quote.value = { ...quote.value, booking: { ...quote.value.booking, ...b } }
|
||||||
if (b.status === 'confirmed' || b.status === 'checked_in' || b.status === 'completed') {
|
if (b.status === 'confirmed' || b.status === 'checked_in' || b.status === 'completed') {
|
||||||
isPaid.value = true
|
isPaid.value = true
|
||||||
pending.remove(b.room_id)
|
|
||||||
stopPolling()
|
stopPolling()
|
||||||
} else if (LOST_BOOKING_STATUSES.has(b.status)) {
|
} else if (LOST_BOOKING_STATUSES.has(b.status)) {
|
||||||
lostStatus.value = b.status
|
lostStatus.value = b.status
|
||||||
pending.remove(b.room_id)
|
|
||||||
stopPolling()
|
stopPolling()
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -185,7 +142,6 @@ export function useBookingFlow() {
|
||||||
hasWalletWithBalance,
|
hasWalletWithBalance,
|
||||||
isPayingWithWallet,
|
isPayingWithWallet,
|
||||||
request,
|
request,
|
||||||
resume,
|
|
||||||
copyInvoice,
|
copyInvoice,
|
||||||
openExternalWallet,
|
openExternalWallet,
|
||||||
payWithWallet,
|
payWithWallet,
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import { storeToRefs } from 'pinia'
|
||||||
import { SERVICE_TOKENS, tryInjectService } from '@/core/di-container'
|
import { SERVICE_TOKENS, tryInjectService } from '@/core/di-container'
|
||||||
import type { ChateletApiService } from '../services/ChateletApiService'
|
import type { ChateletApiService } from '../services/ChateletApiService'
|
||||||
import type { AvailabilityResult, Room } from '../types/room'
|
import type { AvailabilityResult, Room } from '../types/room'
|
||||||
import type { PublicBooking } from '../types/booking'
|
|
||||||
import { useChateletStore } from '../stores/chatelet'
|
import { useChateletStore } from '../stores/chatelet'
|
||||||
|
|
||||||
export function useChatelet() {
|
export function useChatelet() {
|
||||||
|
|
@ -35,11 +34,5 @@ export function useChatelet() {
|
||||||
return api.checkAvailability(roomId, checkIn, checkOut)
|
return api.checkAvailability(roomId, checkIn, checkOut)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Keyless read-back of a booking by id (status, dates, amounts). */
|
return { rooms, isLoading, loadRooms, getRoom, checkAvailability }
|
||||||
async function getPublicBooking(bookingId: string): Promise<PublicBooking | null> {
|
|
||||||
if (!api) return null
|
|
||||||
return api.getPublicBooking(bookingId)
|
|
||||||
}
|
|
||||||
|
|
||||||
return { rooms, isLoading, loadRooms, getRoom, checkAvailability, getPublicBooking }
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,119 +0,0 @@
|
||||||
import type { BookingQuote } from '../types/booking'
|
|
||||||
|
|
||||||
const STORAGE_KEY = 'chatelet:pending-bookings'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A held-but-unpaid booking kept on the device so the guest can return to
|
|
||||||
* its invoice. The public booking read-back strips the bolt11, so the only
|
|
||||||
* way back to the same invoice after the dialog is closed is to have saved
|
|
||||||
* it here.
|
|
||||||
*/
|
|
||||||
export interface PendingBooking {
|
|
||||||
bookingId: string
|
|
||||||
roomId: string
|
|
||||||
checkIn: string
|
|
||||||
checkOut: string
|
|
||||||
nights: number
|
|
||||||
numGuests: number
|
|
||||||
currency: string | null
|
|
||||||
quoteFiat: number | null
|
|
||||||
amountSat: number
|
|
||||||
depositSat: number
|
|
||||||
paymentRequest: string
|
|
||||||
paymentHash: string
|
|
||||||
/** ISO hold expiry, or null. Past-expiry entries are pruned on read. */
|
|
||||||
expiresAt: string | null
|
|
||||||
savedAt: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type Store = Record<string, PendingBooking>
|
|
||||||
|
|
||||||
function readStore(): Store {
|
|
||||||
try {
|
|
||||||
const raw = localStorage.getItem(STORAGE_KEY)
|
|
||||||
const parsed = raw ? JSON.parse(raw) : {}
|
|
||||||
return parsed && typeof parsed === 'object' ? (parsed as Store) : {}
|
|
||||||
} catch {
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function writeStore(store: Store): void {
|
|
||||||
try {
|
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(store))
|
|
||||||
} catch {
|
|
||||||
// Private mode / quota / storage disabled — recovery is best-effort.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isLive(pb: PendingBooking): boolean {
|
|
||||||
if (!pb.expiresAt) return true
|
|
||||||
const t = new Date(pb.expiresAt).getTime()
|
|
||||||
return isNaN(t) || t > Date.now()
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Drop entries whose hold has already expired. Returns whether it changed. */
|
|
||||||
function prune(store: Store): boolean {
|
|
||||||
let changed = false
|
|
||||||
for (const [roomId, pb] of Object.entries(store)) {
|
|
||||||
if (!isLive(pb)) {
|
|
||||||
delete store[roomId]
|
|
||||||
changed = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return changed
|
|
||||||
}
|
|
||||||
|
|
||||||
function fromQuote(q: BookingQuote): PendingBooking {
|
|
||||||
const b = q.booking
|
|
||||||
return {
|
|
||||||
bookingId: b.id,
|
|
||||||
roomId: b.room_id,
|
|
||||||
checkIn: b.check_in,
|
|
||||||
checkOut: b.check_out,
|
|
||||||
nights: b.nights,
|
|
||||||
numGuests: b.num_guests,
|
|
||||||
currency: b.currency,
|
|
||||||
quoteFiat: b.price_fiat,
|
|
||||||
amountSat: b.amount_sat,
|
|
||||||
depositSat: b.deposit_sat,
|
|
||||||
paymentRequest: q.payment_request,
|
|
||||||
paymentHash: q.payment_hash,
|
|
||||||
expiresAt: b.expires_at,
|
|
||||||
savedAt: new Date().toISOString(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Device-local record of held-but-unpaid bookings, one per room (a room
|
|
||||||
* holds at most one booking at a time). Lets a guest who closed the invoice
|
|
||||||
* dialog return and finish paying instead of waiting out the hold. Works
|
|
||||||
* without an account — the store is this browser's, not the server's.
|
|
||||||
*/
|
|
||||||
export function usePendingBooking() {
|
|
||||||
function save(q: BookingQuote): void {
|
|
||||||
if (!q.payment_request || !q.booking?.id) return
|
|
||||||
const store = readStore()
|
|
||||||
prune(store)
|
|
||||||
store[q.booking.room_id] = fromQuote(q)
|
|
||||||
writeStore(store)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getForRoom(roomId: string): PendingBooking | null {
|
|
||||||
if (!roomId) return null
|
|
||||||
const store = readStore()
|
|
||||||
if (prune(store)) writeStore(store)
|
|
||||||
return store[roomId] ?? null
|
|
||||||
}
|
|
||||||
|
|
||||||
function remove(roomId: string): void {
|
|
||||||
if (!roomId) return
|
|
||||||
const store = readStore()
|
|
||||||
if (store[roomId]) {
|
|
||||||
delete store[roomId]
|
|
||||||
writeStore(store)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { save, getForRoom, remove }
|
|
||||||
}
|
|
||||||
|
|
@ -3,7 +3,6 @@ import { computed, onMounted, ref, watch } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { toast } from 'vue-sonner'
|
import { toast } from 'vue-sonner'
|
||||||
import { watchDebounced } from '@vueuse/core'
|
import { watchDebounced } from '@vueuse/core'
|
||||||
import { format } from 'date-fns'
|
|
||||||
import { Clock, MapPin, Moon, Users } from 'lucide-vue-next'
|
import { Clock, MapPin, Moon, Users } from 'lucide-vue-next'
|
||||||
import { Card, CardContent } from '@/components/ui/card'
|
import { Card, CardContent } from '@/components/ui/card'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
|
@ -13,9 +12,7 @@ import { formatSats } from '@/lib/utils/formatting'
|
||||||
import { useAuth } from '@/composables/useAuthService'
|
import { useAuth } from '@/composables/useAuthService'
|
||||||
import { useChatelet } from '../composables/useChatelet'
|
import { useChatelet } from '../composables/useChatelet'
|
||||||
import { useRoomUnavailability } from '../composables/useRoomUnavailability'
|
import { useRoomUnavailability } from '../composables/useRoomUnavailability'
|
||||||
import { usePendingBooking, type PendingBooking } from '../composables/usePendingBooking'
|
|
||||||
import { stayIsFree } from '../lib/unavailable'
|
import { stayIsFree } from '../lib/unavailable'
|
||||||
import { LOST_BOOKING_STATUSES } from '../types/booking'
|
|
||||||
import BookRoomDialog from '../components/BookRoomDialog.vue'
|
import BookRoomDialog from '../components/BookRoomDialog.vue'
|
||||||
import StayDatePicker from '../components/StayDatePicker.vue'
|
import StayDatePicker from '../components/StayDatePicker.vue'
|
||||||
import type { AvailabilityResult, Room } from '../types/room'
|
import type { AvailabilityResult, Room } from '../types/room'
|
||||||
|
|
@ -23,8 +20,7 @@ import type { AvailabilityResult, Room } from '../types/room'
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { isAuthenticated } = useAuth()
|
const { isAuthenticated } = useAuth()
|
||||||
const { getRoom, checkAvailability, getPublicBooking } = useChatelet()
|
const { getRoom, checkAvailability } = useChatelet()
|
||||||
const pendingStore = usePendingBooking()
|
|
||||||
|
|
||||||
const room = ref<Room | null>(null)
|
const room = ref<Room | null>(null)
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
|
|
@ -36,11 +32,6 @@ const showBooking = ref(false)
|
||||||
/** Quote snapshot the dialog books against — survives the page's own
|
/** Quote snapshot the dialog books against — survives the page's own
|
||||||
* `result` being cleared once the dates are taken. */
|
* `result` being cleared once the dates are taken. */
|
||||||
const bookingQuote = ref<AvailabilityResult | null>(null)
|
const bookingQuote = ref<AvailabilityResult | null>(null)
|
||||||
/** A held-but-unpaid booking saved on this device, if the guest closed the
|
|
||||||
* invoice earlier — surfaced as a "resume" banner. */
|
|
||||||
const pendingBooking = ref<PendingBooking | null>(null)
|
|
||||||
/** Passed to the dialog to reopen the saved invoice instead of the form. */
|
|
||||||
const resumePayload = ref<PendingBooking | null>(null)
|
|
||||||
|
|
||||||
// Occupied nights for the calendar; refreshed after a booking and when a
|
// Occupied nights for the calendar; refreshed after a booking and when a
|
||||||
// quote contradicts the local set (someone else's hold appeared).
|
// quote contradicts the local set (someone else's hold appeared).
|
||||||
|
|
@ -77,59 +68,6 @@ onMounted(async () => {
|
||||||
checkIn.value = qi
|
checkIn.value = qi
|
||||||
checkOut.value = qo
|
checkOut.value = qo
|
||||||
}
|
}
|
||||||
await refreshPending()
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Surface a "resume payment" banner if this device holds an unpaid booking
|
|
||||||
* for this room. Verify server-side first: drop it if it already confirmed
|
|
||||||
* or the hold lapsed (the store also self-prunes past-expiry entries).
|
|
||||||
*/
|
|
||||||
async function refreshPending() {
|
|
||||||
const id = roomIdRef.value
|
|
||||||
if (!id) return
|
|
||||||
const pb = pendingStore.getForRoom(id)
|
|
||||||
if (!pb) {
|
|
||||||
pendingBooking.value = null
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const live = await getPublicBooking(pb.bookingId)
|
|
||||||
const status = live?.status
|
|
||||||
if (!status || status === 'confirmed' || status === 'checked_in' || status === 'completed' || LOST_BOOKING_STATUSES.has(status)) {
|
|
||||||
pendingStore.remove(id)
|
|
||||||
pendingBooking.value = null
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Network hiccup — keep the local record and let the resumed poll decide.
|
|
||||||
}
|
|
||||||
pendingBooking.value = pb
|
|
||||||
}
|
|
||||||
|
|
||||||
function resumeBooking() {
|
|
||||||
const pb = pendingBooking.value
|
|
||||||
if (!pb || !room.value) return
|
|
||||||
// Rebuild the availability shape the dialog header reads from the saved hold.
|
|
||||||
bookingQuote.value = {
|
|
||||||
room_id: pb.roomId,
|
|
||||||
check_in: pb.checkIn,
|
|
||||||
check_out: pb.checkOut,
|
|
||||||
available: true,
|
|
||||||
nights: pb.nights,
|
|
||||||
quote_sat: pb.amountSat,
|
|
||||||
quote_fiat: pb.quoteFiat,
|
|
||||||
currency: pb.currency,
|
|
||||||
}
|
|
||||||
resumePayload.value = pb
|
|
||||||
showBooking.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const pendingStayLabel = computed(() => {
|
|
||||||
const pb = pendingBooking.value
|
|
||||||
if (!pb) return ''
|
|
||||||
const fmt = (ymd: string) => format(new Date(ymd + 'T00:00:00'), 'MMM d')
|
|
||||||
return `${fmt(pb.checkIn)} → ${fmt(pb.checkOut)}`
|
|
||||||
})
|
})
|
||||||
|
|
||||||
async function onCheck() {
|
async function onCheck() {
|
||||||
|
|
@ -178,17 +116,11 @@ function onBooked() {
|
||||||
result.value = null
|
result.value = null
|
||||||
checkIn.value = ''
|
checkIn.value = ''
|
||||||
checkOut.value = ''
|
checkOut.value = ''
|
||||||
pendingBooking.value = null
|
|
||||||
void unavailability.refresh()
|
void unavailability.refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(showBooking, (open) => {
|
watch(showBooking, (open) => {
|
||||||
if (!open) {
|
if (!open) bookingQuote.value = null
|
||||||
bookingQuote.value = null
|
|
||||||
resumePayload.value = null
|
|
||||||
// The hold may have been paid or lapsed while the dialog was open.
|
|
||||||
void refreshPending()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -266,18 +198,6 @@ watch(showBooking, (open) => {
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card v-if="pendingBooking && !showBooking" class="border-primary/40 bg-primary/5">
|
|
||||||
<CardContent class="p-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
||||||
<div class="space-y-0.5">
|
|
||||||
<p class="text-sm font-medium text-foreground">You have an unpaid hold</p>
|
|
||||||
<p class="text-sm text-muted-foreground">
|
|
||||||
{{ pendingStayLabel }} · {{ formatSats(pendingBooking.depositSat) }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Button size="sm" class="shrink-0" @click="resumeBooking">Resume payment</Button>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent class="p-4 space-y-4">
|
<CardContent class="p-4 space-y-4">
|
||||||
<StayDatePicker
|
<StayDatePicker
|
||||||
|
|
@ -323,7 +243,6 @@ watch(showBooking, (open) => {
|
||||||
v-if="room && bookingQuote"
|
v-if="room && bookingQuote"
|
||||||
:room="room"
|
:room="room"
|
||||||
:availability="bookingQuote"
|
:availability="bookingQuote"
|
||||||
:resume="resumePayload"
|
|
||||||
v-model:is-open="showBooking"
|
v-model:is-open="showBooking"
|
||||||
@booked="onBooked"
|
@booked="onBooked"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue