diff --git a/src/components/ui/range-calendar/RangeCalendar.vue b/src/components/ui/range-calendar/RangeCalendar.vue
deleted file mode 100644
index 2d11816..0000000
--- a/src/components/ui/range-calendar/RangeCalendar.vue
+++ /dev/null
@@ -1,58 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ day }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/components/ui/range-calendar/RangeCalendarCell.vue b/src/components/ui/range-calendar/RangeCalendarCell.vue
deleted file mode 100644
index e5dea17..0000000
--- a/src/components/ui/range-calendar/RangeCalendarCell.vue
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-
-
-
-
-
diff --git a/src/components/ui/range-calendar/RangeCalendarCellTrigger.vue b/src/components/ui/range-calendar/RangeCalendarCellTrigger.vue
deleted file mode 100644
index 7971c55..0000000
--- a/src/components/ui/range-calendar/RangeCalendarCellTrigger.vue
+++ /dev/null
@@ -1,40 +0,0 @@
-
-
-
-
-
-
-
diff --git a/src/components/ui/range-calendar/RangeCalendarGrid.vue b/src/components/ui/range-calendar/RangeCalendarGrid.vue
deleted file mode 100644
index c1db6c9..0000000
--- a/src/components/ui/range-calendar/RangeCalendarGrid.vue
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-
-
-
-
-
diff --git a/src/components/ui/range-calendar/RangeCalendarGridBody.vue b/src/components/ui/range-calendar/RangeCalendarGridBody.vue
deleted file mode 100644
index 68556dd..0000000
--- a/src/components/ui/range-calendar/RangeCalendarGridBody.vue
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
diff --git a/src/components/ui/range-calendar/RangeCalendarGridHead.vue b/src/components/ui/range-calendar/RangeCalendarGridHead.vue
deleted file mode 100644
index f6cc09e..0000000
--- a/src/components/ui/range-calendar/RangeCalendarGridHead.vue
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
-
-
diff --git a/src/components/ui/range-calendar/RangeCalendarGridRow.vue b/src/components/ui/range-calendar/RangeCalendarGridRow.vue
deleted file mode 100644
index 717396c..0000000
--- a/src/components/ui/range-calendar/RangeCalendarGridRow.vue
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
-
-
diff --git a/src/components/ui/range-calendar/RangeCalendarHeadCell.vue b/src/components/ui/range-calendar/RangeCalendarHeadCell.vue
deleted file mode 100644
index 7ea377f..0000000
--- a/src/components/ui/range-calendar/RangeCalendarHeadCell.vue
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
-
-
diff --git a/src/components/ui/range-calendar/RangeCalendarHeader.vue b/src/components/ui/range-calendar/RangeCalendarHeader.vue
deleted file mode 100644
index 5cef2d5..0000000
--- a/src/components/ui/range-calendar/RangeCalendarHeader.vue
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
-
-
diff --git a/src/components/ui/range-calendar/RangeCalendarHeading.vue b/src/components/ui/range-calendar/RangeCalendarHeading.vue
deleted file mode 100644
index 90845e3..0000000
--- a/src/components/ui/range-calendar/RangeCalendarHeading.vue
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-
- {{ headingValue }}
-
-
-
diff --git a/src/components/ui/range-calendar/RangeCalendarNextButton.vue b/src/components/ui/range-calendar/RangeCalendarNextButton.vue
deleted file mode 100644
index f60c786..0000000
--- a/src/components/ui/range-calendar/RangeCalendarNextButton.vue
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
-
-
-
-
-
-
diff --git a/src/components/ui/range-calendar/RangeCalendarPrevButton.vue b/src/components/ui/range-calendar/RangeCalendarPrevButton.vue
deleted file mode 100644
index 8ed2032..0000000
--- a/src/components/ui/range-calendar/RangeCalendarPrevButton.vue
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
-
-
-
-
-
-
diff --git a/src/components/ui/range-calendar/index.ts b/src/components/ui/range-calendar/index.ts
deleted file mode 100644
index 34d5361..0000000
--- a/src/components/ui/range-calendar/index.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-export { default as RangeCalendar } from "./RangeCalendar.vue"
-export { default as RangeCalendarCell } from "./RangeCalendarCell.vue"
-export { default as RangeCalendarCellTrigger } from "./RangeCalendarCellTrigger.vue"
-export { default as RangeCalendarGrid } from "./RangeCalendarGrid.vue"
-export { default as RangeCalendarGridBody } from "./RangeCalendarGridBody.vue"
-export { default as RangeCalendarGridHead } from "./RangeCalendarGridHead.vue"
-export { default as RangeCalendarGridRow } from "./RangeCalendarGridRow.vue"
-export { default as RangeCalendarHeadCell } from "./RangeCalendarHeadCell.vue"
-export { default as RangeCalendarHeader } from "./RangeCalendarHeader.vue"
-export { default as RangeCalendarHeading } from "./RangeCalendarHeading.vue"
-export { default as RangeCalendarNextButton } from "./RangeCalendarNextButton.vue"
-export { default as RangeCalendarPrevButton } from "./RangeCalendarPrevButton.vue"
diff --git a/src/modules/chatelet/components/StayDatePicker.vue b/src/modules/chatelet/components/StayDatePicker.vue
deleted file mode 100644
index 3c2ee0e..0000000
--- a/src/modules/chatelet/components/StayDatePicker.vue
+++ /dev/null
@@ -1,260 +0,0 @@
-
-
-
-
-
-
-
-
{{ title }}
-
{{ subtitle }}
-
-
-
-
Check-in
-
-
- {{ checkIn ? pretty(checkIn) : 'Add date' }}
-
-
-
-
-
-
Check-out
-
-
- {{ checkOut ? pretty(checkOut) : 'Add date' }}
-
-
-
-
-
-
-
-
-
-
-
-
- Unavailable
-
-
-
-
-
diff --git a/src/modules/chatelet/composables/useRoomUnavailability.ts b/src/modules/chatelet/composables/useRoomUnavailability.ts
deleted file mode 100644
index 684b931..0000000
--- a/src/modules/chatelet/composables/useRoomUnavailability.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-import { computed, ref, watch, type Ref } from 'vue'
-import { getLocalTimeZone, today } from '@internationalized/date'
-import { SERVICE_TOKENS, tryInjectService } from '@/core/di-container'
-import type { ChateletApiService } from '../services/ChateletApiService'
-import { addDays, expandRanges } from '../lib/unavailable'
-
-const WINDOW_DAYS = 365
-
-/**
- * The occupied nights of one room for the calendar. One fetch of a 12-month
- * window per room — a few spans, cheap — and the calendar's `maxValue` is
- * pinned to the window end, so no refetch on month navigation. `refresh()`
- * after a booking lands, or when a quote comes back "not available" for a
- * stay the local set thought free (someone else's hold appeared).
- */
-export function useRoomUnavailability(roomId: Ref) {
- // DI, never a direct import (workspace rule).
- const api = tryInjectService(SERVICE_TOKENS.CHATELET_API)
-
- const nights = ref([])
- const windowStart = ref(today(getLocalTimeZone()).toString())
- const windowEnd = ref(addDays(windowStart.value, WINDOW_DAYS))
- const isLoading = ref(false)
- const error = ref(null)
-
- const nightSet = computed(() => new Set(nights.value))
-
- async function refresh(): Promise {
- if (!api || !roomId.value) return
- isLoading.value = true
- error.value = null
- try {
- const start = today(getLocalTimeZone()).toString()
- const res = await api.getUnavailableRanges(roomId.value, start, addDays(start, WINDOW_DAYS))
- windowStart.value = res.start
- windowEnd.value = res.end
- nights.value = expandRanges(res.ranges)
- } catch (e: unknown) {
- error.value = e instanceof Error ? e.message : 'Could not load availability'
- } finally {
- isLoading.value = false
- }
- }
-
- watch(roomId, () => void refresh(), { immediate: true })
-
- return { nights, nightSet, windowStart, windowEnd, isLoading, error, refresh }
-}
diff --git a/src/modules/chatelet/lib/unavailable.spec.ts b/src/modules/chatelet/lib/unavailable.spec.ts
deleted file mode 100644
index 2756ce6..0000000
--- a/src/modules/chatelet/lib/unavailable.spec.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import { describe, expect, it } from 'vitest'
-import {
- addDays,
- expandRanges,
- nextUnavailableOnOrAfter,
- nightsBetween,
- stayIsFree,
-} from './unavailable'
-
-describe('unavailable-night helpers', () => {
- it('addDays crosses month and year boundaries', () => {
- expect(addDays('2026-10-31', 1)).toBe('2026-11-01')
- expect(addDays('2026-12-31', 1)).toBe('2027-01-01')
- expect(addDays('2026-03-01', -1)).toBe('2026-02-28')
- })
-
- it('nightsBetween is check_out-exclusive', () => {
- expect(nightsBetween('2026-10-05', '2026-10-07')).toBe(2)
- expect(nightsBetween('2026-10-05', '2026-10-05')).toBe(0)
- })
-
- it('expandRanges yields sorted, de-duplicated occupied nights (end exclusive)', () => {
- const nights = expandRanges([
- { start: '2026-10-10', end: '2026-10-12' },
- { start: '2026-10-05', end: '2026-10-07' },
- { start: '2026-10-11', end: '2026-10-13' },
- ])
- expect(nights).toEqual(['2026-10-05', '2026-10-06', '2026-10-10', '2026-10-11', '2026-10-12'])
- })
-
- it('nextUnavailableOnOrAfter finds the boundary night', () => {
- const nights = ['2026-10-05', '2026-10-06', '2026-10-10']
- expect(nextUnavailableOnOrAfter(nights, '2026-10-01')).toBe('2026-10-05')
- expect(nextUnavailableOnOrAfter(nights, '2026-10-05')).toBe('2026-10-05')
- expect(nextUnavailableOnOrAfter(nights, '2026-10-07')).toBe('2026-10-10')
- expect(nextUnavailableOnOrAfter(nights, '2026-10-11')).toBeUndefined()
- expect(nextUnavailableOnOrAfter([], '2026-10-11')).toBeUndefined()
- })
-
- it('stayIsFree honours half-open semantics', () => {
- const set = new Set(['2026-10-05', '2026-10-06'])
- // checking out the morning the other guest checks in is fine
- expect(stayIsFree(set, '2026-10-03', '2026-10-05')).toBe(true)
- // checking in the morning they leave is fine too
- expect(stayIsFree(set, '2026-10-07', '2026-10-09')).toBe(true)
- // spanning an occupied night is not
- expect(stayIsFree(set, '2026-10-04', '2026-10-06')).toBe(false)
- expect(stayIsFree(set, '2026-10-06', '2026-10-06')).toBe(false)
- })
-})
diff --git a/src/modules/chatelet/lib/unavailable.ts b/src/modules/chatelet/lib/unavailable.ts
deleted file mode 100644
index 0904b4c..0000000
--- a/src/modules/chatelet/lib/unavailable.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import type { DateRange } from '../types/room'
-
-/** YYYY-MM-DD → YYYY-MM-DD, `days` later. Date-only math, no timezone. */
-export function addDays(ymd: string, days: number): string {
- const d = new Date(ymd + 'T00:00:00Z')
- d.setUTCDate(d.getUTCDate() + days)
- return d.toISOString().slice(0, 10)
-}
-
-/** Nights between two YYYY-MM-DD dates (check_out exclusive). */
-export function nightsBetween(checkIn: string, checkOut: string): number {
- const a = Date.UTC(+checkIn.slice(0, 4), +checkIn.slice(5, 7) - 1, +checkIn.slice(8, 10))
- const b = Date.UTC(+checkOut.slice(0, 4), +checkOut.slice(5, 7) - 1, +checkOut.slice(8, 10))
- return Math.round((b - a) / 86_400_000)
-}
-
-/**
- * Expand half-open [start, end) spans into the sorted, de-duplicated list of
- * occupied nights. A year of a busy room is a few hundred strings — cheap to
- * hold and to binary-search.
- */
-export function expandRanges(ranges: DateRange[]): string[] {
- const out = new Set()
- for (const r of ranges) {
- for (let d = r.start; d < r.end; d = addDays(d, 1)) out.add(d)
- }
- return [...out].sort()
-}
-
-/** First occupied night ≥ `ymd`, or undefined if none in the loaded window. */
-export function nextUnavailableOnOrAfter(nights: string[], ymd: string): string | undefined {
- let lo = 0
- let hi = nights.length
- while (lo < hi) {
- const mid = (lo + hi) >> 1
- if (nights[mid] < ymd) lo = mid + 1
- else hi = mid
- }
- return nights[lo]
-}
-
-/** True when every night of [checkIn, checkOut) is free. */
-export function stayIsFree(nightSet: Set, checkIn: string, checkOut: string): boolean {
- if (!(checkIn < checkOut)) return false
- for (let d = checkIn; d < checkOut; d = addDays(d, 1)) {
- if (nightSet.has(d)) return false
- }
- return true
-}
diff --git a/src/modules/chatelet/services/ChateletApiService.ts b/src/modules/chatelet/services/ChateletApiService.ts
index 36e74f3..2be1400 100644
--- a/src/modules/chatelet/services/ChateletApiService.ts
+++ b/src/modules/chatelet/services/ChateletApiService.ts
@@ -1,4 +1,4 @@
-import type { AvailabilityResult, Room, UnavailableRanges } from '../types/room'
+import type { AvailabilityResult, Room } from '../types/room'
import type { BookingQuote, BookingRequest, PublicBooking } from '../types/booking'
export interface ChateletApiConfig {
@@ -30,26 +30,6 @@ export class ChateletApiService {
return this.request(`/chatelet/api/v1/public/rooms/${id}`, { method: 'GET' })
}
- /**
- * Nights a guest cannot book over a window (default today → +365 d): live
- * holds, confirmed stays and manual blocks merged into anonymous spans.
- * Keyless, like the room read it hangs off.
- */
- async getUnavailableRanges(
- roomId: string,
- start?: string,
- end?: string,
- ): Promise {
- const q = new URLSearchParams()
- if (start) q.set('start', start)
- if (end) q.set('end', end)
- const qs = q.toString()
- return this.request(
- `/chatelet/api/v1/public/rooms/${roomId}/unavailable${qs ? `?${qs}` : ''}`,
- { method: 'GET' },
- )
- }
-
async checkAvailability(
roomId: string,
checkIn: string,
diff --git a/src/modules/chatelet/types/room.ts b/src/modules/chatelet/types/room.ts
index b49ebef..6800d6a 100644
--- a/src/modules/chatelet/types/room.ts
+++ b/src/modules/chatelet/types/room.ts
@@ -31,17 +31,3 @@ export interface AvailabilityResult {
quote_fiat: number | null
currency: string | null
}
-
-/** Half-open [start, end) span of occupied nights; `end` is the morning the room frees up. */
-export interface DateRange {
- start: string
- end: string
-}
-
-/** `GET /chatelet/api/v1/public/rooms/{id}/unavailable` — the guest calendar feed. */
-export interface UnavailableRanges {
- room_id: string
- start: string
- end: string
- ranges: DateRange[]
-}
diff --git a/src/modules/chatelet/views/ChateletDetailPage.vue b/src/modules/chatelet/views/ChateletDetailPage.vue
index aa71891..ae43195 100644
--- a/src/modules/chatelet/views/ChateletDetailPage.vue
+++ b/src/modules/chatelet/views/ChateletDetailPage.vue
@@ -2,18 +2,16 @@
import { computed, onMounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { toast } from 'vue-sonner'
-import { watchDebounced } from '@vueuse/core'
+import { addDays, format } from 'date-fns'
import { MapPin, Users } from 'lucide-vue-next'
import { Card, CardContent } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
-import { formatSats } from '@/lib/utils/formatting'
+import { Label } from '@/components/ui/label'
+import DatePicker from '@/modules/base/components/DatePicker.vue'
import { useAuth } from '@/composables/useAuthService'
import { useChatelet } from '../composables/useChatelet'
-import { useRoomUnavailability } from '../composables/useRoomUnavailability'
-import { stayIsFree } from '../lib/unavailable'
import BookRoomDialog from '../components/BookRoomDialog.vue'
-import StayDatePicker from '../components/StayDatePicker.vue'
import type { AvailabilityResult, Room } from '../types/room'
const route = useRoute()
@@ -32,23 +30,25 @@ const showBooking = ref(false)
* `result` being cleared once the dates are taken. */
const bookingQuote = ref(null)
-// Occupied nights for the calendar; refreshed after a booking and when a
-// quote contradicts the local set (someone else's hold appeared).
-const roomIdRef = computed(() => route.params.id as string | undefined)
-const unavailability = useRoomUnavailability(roomIdRef)
+// Same YYYY-MM-DD wire format the DatePicker speaks; the availability
+// endpoint takes the same strings.
+const today = computed(() => format(new Date(), 'yyyy-MM-dd'))
-// Any date change invalidates the last quote; a complete stay re-quotes
-// itself (the availability call is the price preview, so no button).
-watch([checkIn, checkOut], () => {
+// Check-out must be at least the next morning. Bumping the floor also
+// clears a check-out that the new check-in has overtaken.
+const minCheckOut = computed(() =>
+ checkIn.value
+ ? format(addDays(new Date(checkIn.value + 'T00:00:00'), 1), 'yyyy-MM-dd')
+ : today.value,
+)
+
+watch(checkIn, () => {
+ if (checkOut.value && checkOut.value < minCheckOut.value) checkOut.value = ''
+ result.value = null
+})
+watch(checkOut, () => {
result.value = null
})
-watchDebounced(
- [checkIn, checkOut],
- ([ci, co]) => {
- if (ci && co && room.value) void onCheck()
- },
- { debounce: 150 },
-)
const YMD = /^\d{4}-\d{2}-\d{2}$/
@@ -66,6 +66,7 @@ onMounted(async () => {
if (typeof qi === 'string' && YMD.test(qi) && typeof qo === 'string' && YMD.test(qo)) {
checkIn.value = qi
checkOut.value = qo
+ if (room.value) await onCheck()
}
})
@@ -74,16 +75,7 @@ async function onCheck() {
checking.value = true
result.value = null
try {
- const ci = checkIn.value
- const co = checkOut.value
- const r = await checkAvailability(room.value.id, ci, co)
- // Only the quote for the dates still selected counts (debounce race).
- if (ci !== checkIn.value || co !== checkOut.value) return
- result.value = r
- if (r && !r.available && stayIsFree(unavailability.nightSet.value, ci, co)) {
- // The calendar thought these nights were free — it is stale.
- void unavailability.refresh()
- }
+ result.value = await checkAvailability(room.value.id, checkIn.value, checkOut.value)
} catch (e: unknown) {
toast.error(e instanceof Error ? e.message : 'Availability check failed')
} finally {
@@ -110,12 +102,11 @@ function onBook() {
showBooking.value = true
}
-/** The dates are taken now — drop the stale quote and grey them out. */
+/** The dates are taken now — drop the stale quote so the card resets. */
function onBooked() {
result.value = null
checkIn.value = ''
checkOut.value = ''
- void unavailability.refresh()
}
watch(showBooking, (open) => {
@@ -162,36 +153,39 @@ watch(showBooking, (open) => {
-
-
-
- Could not load the availability calendar — you can still pick dates and check them.
-
-
-
-
- Checking availability…
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-
- {{ room.price_amount }} {{ room.price_currency }} × {{ result.nights }} night{{ result.nights === 1 ? '' : 's' }}
- {{ result.quote_fiat }} {{ result.currency }}
-
-
- Total
- {{ formatSats(result.quote_sat) }}
-
-
+
+ Available for {{ result.nights }} night(s) —
+ {{ result.quote_sat }} sats
+
+ (≈ {{ result.quote_fiat }} {{ result.currency }})
+
+