feat(planner): reach a hotel's booking from the plan, not only its place

A booked stay's pill in the day head gets a second target that opens the booking; the name still opens the place. The place panel of a hotel, on the desktop and on the phone, lists the stay booked for it next to the bookings pinned to the stop. (#2363)
This commit is contained in:
Maurice
2026-10-01 09:43:53 +02:00
parent 104f0eef38
commit c80ee6b12a
34 changed files with 155 additions and 17 deletions
@@ -4338,6 +4338,44 @@ describe('DayPlanSidebar', () => {
expect(onPlaceClick).toHaveBeenCalledWith(8)
})
it('FE-PLANNER-DAYPLAN-176b: a booked stay opens its booking from the pill, the name still opens the place (#2363)', async () => {
const user = userEvent.setup()
const days = [
buildDay({ id: 10, date: '2025-06-01', title: 'Day 1' }),
buildDay({ id: 11, date: '2025-06-02', title: 'Day 2' }),
]
const accommodations: Accommodation[] = [
{ id: 3, trip_id: 1, start_day_id: 10, end_day_id: 11, place_id: 9, place_lat: 51.5, place_lng: -0.12, place_name: 'Booked Hotel' },
{ id: 4, trip_id: 1, start_day_id: 10, end_day_id: 11, place_id: 8, place_lat: 51.5, place_lng: -0.12, place_name: 'Unbooked Hotel' },
]
const stay = buildReservation({ id: 610, type: 'hotel', title: 'Booked Hotel stay', accommodation_id: 3 } as any)
const onPlaceClick = vi.fn()
const onOpenBooking = vi.fn()
render(<DayPlanSidebar {...makeDefaultProps({ days, accommodations, reservations: [stay], onPlaceClick, onOpenBooking })} />)
const header = dayHeader('Day 1')
const bookingButtons = header.querySelectorAll('[data-dp="day-pill-booking"]')
// Only the stay that has a booking gets the second target.
expect(bookingButtons).toHaveLength(1)
expect(bookingButtons[0]).toHaveAccessibleName('Open booking')
await user.click(bookingButtons[0] as HTMLElement)
expect(onOpenBooking).toHaveBeenCalledWith(stay)
expect(onPlaceClick).not.toHaveBeenCalled()
await user.click(within(header).getByText('Booked Hotel'))
expect(onPlaceClick).toHaveBeenCalledWith(9)
})
it('FE-PLANNER-DAYPLAN-176c: without a booking detail to open, the stay pill has no booking button', () => {
const days = [buildDay({ id: 10, date: '2025-06-01', title: 'Day 1' }), buildDay({ id: 11, date: '2025-06-02', title: 'Day 2' })]
const accommodations: Accommodation[] = [
{ id: 3, trip_id: 1, start_day_id: 10, end_day_id: 11, place_id: 9, place_lat: 51.5, place_lng: -0.12, place_name: 'Booked Hotel' },
]
const stay = buildReservation({ id: 610, type: 'hotel', title: 'Booked Hotel stay', accommodation_id: 3 } as any)
render(<DayPlanSidebar {...makeDefaultProps({ days, accommodations, reservations: [stay] })} />)
expect(dayHeader('Day 1').querySelector('[data-dp="day-pill-booking"]')).toBeNull()
})
it('FE-PLANNER-DAYPLAN-177: clicking an active rental badge opens its booking detail', async () => {
const user = userEvent.setup()
const days = [
@@ -1898,16 +1898,32 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
const pending = pendingStays.has(acc.id)
const named = state ? `${state}: ${accName}` : accName
const tip = pending ? `${named} (${t('reservations.pending')})` : named
// The name opens the place, as before; a stay with a booking gets a
// second target in the same pill that opens the booking (#2363).
const stayBooking = onOpenBooking
? reservations.find(r => r.accommodation_id != null && Number(r.accommodation_id) === acc.id)
: undefined
return (
<Tooltip key={acc.id} label={tip}>
<button type="button" data-dp="day-pill" data-pending={pending || undefined}
onClick={e => { e.stopPropagation(); if ((acc as any).place_id) onPlaceClick((acc as any).place_id) }}
className={`inline-flex min-w-0 max-w-full items-center gap-1 rounded-full px-2 py-[2px] font-geist font-normal text-content-secondary hover:text-content ${pending ? 'border border-dashed border-warning bg-warning-soft' : 'bg-surface-card shadow-sm'}`}
style={{ ...fs(10.5), cursor: (acc as any).place_id ? 'pointer' : 'default' }}>
<Hotel size={11} strokeWidth={2} className={`flex-none ${iconTone}`} />
<span className="truncate">{accName}</span>
</button>
</Tooltip>
<span key={acc.id} className={`inline-flex min-w-0 max-w-full items-stretch overflow-hidden rounded-full ${pending ? 'border border-dashed border-warning bg-warning-soft' : 'bg-surface-card shadow-sm'}`}>
<Tooltip label={tip}>
<button type="button" data-dp="day-pill" data-pending={pending || undefined}
onClick={e => { e.stopPropagation(); if ((acc as any).place_id) onPlaceClick((acc as any).place_id) }}
className={`inline-flex min-w-0 items-center gap-1 py-[2px] font-geist font-normal text-content-secondary hover:text-content ${stayBooking ? 'pl-2 pr-1.5' : 'px-2'}`}
style={{ ...fs(10.5), cursor: (acc as any).place_id ? 'pointer' : 'default' }}>
<Hotel size={11} strokeWidth={2} className={`flex-none ${iconTone}`} />
<span className="truncate">{accName}</span>
</button>
</Tooltip>
{stayBooking && (
<Tooltip label={t('day.openStayBooking')}>
<button type="button" data-dp="day-pill-booking" aria-label={t('day.openStayBooking')}
onClick={e => { e.stopPropagation(); onOpenBooking!(stayBooking) }}
className="flex flex-none items-center border-l border-edge-faint pl-1.5 pr-2 text-content-faint hover:text-content">
<Ticket size={11} strokeWidth={2} />
</button>
</Tooltip>
)}
</span>
)
})}
{/* Active rental car badges */}
@@ -430,6 +430,20 @@ describe('PlaceInspector', () => {
expect(screen.getByText('Museum Ticket')).toBeTruthy();
});
it('FE-PLANNER-INSPECTOR-030h: a hotel shows the stay booked for it and opens that booking (#2363)', () => {
const onOpenBooking = vi.fn();
const stay = buildReservation({
id: 540, title: 'Hotel Adlon, 3 nights', status: 'confirmed', type: 'hotel', assignment_id: null,
accommodation_place_id: place.id,
} as any);
const elsewhere = buildReservation({ id: 541, title: 'Other hotel', type: 'hotel', accommodation_place_id: place.id + 1 } as any);
render(<PlaceInspector {...defaultProps} reservations={[stay, elsewhere]} onOpenBooking={onOpenBooking} />);
expect(screen.queryByText('Other hotel')).toBeNull();
fireEvent.click(screen.getByText('Hotel Adlon, 3 nights').closest('[role="button"]') as HTMLElement);
expect(onOpenBooking).toHaveBeenCalledWith(stay);
});
it('FE-PLANNER-INSPECTOR-030g: every booking on the stop gets its own strip (#2201)', () => {
const onEditReservation = vi.fn();
const parking = buildReservation({
@@ -34,7 +34,7 @@ import { useTripStore } from '../../store/tripStore'
import { useCanDo } from '../../store/permissionsStore'
import { formatDistance, formatElevation } from '../../utils/units'
import { navigationTargetLabel, getNavigationTargets, openNavigationTarget } from './placeNavigation'
import { TRANSPORT_TYPES, getAssignmentReservations } from '../../utils/dayMerge'
import { TRANSPORT_TYPES, getPlaceBookings } from '../../utils/dayMerge'
import { NavigationMenu } from '../shared/NavigationMenu'
import { resolveOpenNow, resolvePlaceTimeZone, placeWeekdayIndex, type OpeningPeriod } from './placeOpenState'
import { convertHoursLine } from './placeHoursFormat'
@@ -479,7 +479,7 @@ export default function PlaceInspector({
{/* Bookings + participants — trip-only (collections have no days) */}
{mode === 'trip' && (
<PlaceBookingsAndPeople selectedAssignmentId={selectedAssignmentId} reservations={reservations}
<PlaceBookingsAndPeople selectedAssignmentId={selectedAssignmentId} placeId={place.id} reservations={reservations}
assignments={assignments} selectedDayId={selectedDayId} tripMembers={tripMembers}
timeFormat={timeFormat} onSetParticipants={onSetParticipants}
onEditTransport={onEditTransport} onEditReservation={onEditReservation} onOpenBooking={onOpenBooking} />
@@ -758,6 +758,7 @@ function InspectorHead({ place, category, openNow, phone, rating, ratingCount, p
interface PlaceBookingsAndPeopleProps {
selectedAssignmentId: number | null
placeId: number
reservations: Reservation[]
assignments: AssignmentsMap
selectedDayId: number | null
@@ -770,10 +771,10 @@ interface PlaceBookingsAndPeopleProps {
}
/** The bookings pinned to this stop and who joins it, side by side from sm up. */
function PlaceBookingsAndPeople({ selectedAssignmentId, reservations, assignments, selectedDayId,
function PlaceBookingsAndPeople({ selectedAssignmentId, placeId, reservations, assignments, selectedDayId,
tripMembers, timeFormat, onSetParticipants, onEditTransport, onEditReservation, onOpenBooking }: PlaceBookingsAndPeopleProps) {
const { t } = useTranslation()
const linked = getAssignmentReservations<Reservation>(reservations, selectedAssignmentId)
const linked = getPlaceBookings<Reservation>(reservations, selectedAssignmentId, placeId)
const assignment = selectedAssignmentId ? (assignments[String(selectedDayId)] || []).find(a => a.id === selectedAssignmentId) : null
const currentParticipants = assignment?.participants || []
const participantIds = currentParticipants.map(p => p.user_id)
@@ -24,7 +24,7 @@ import { openFile } from '../../../../utils/fileDownload'
import { filesForPlace } from '../../../../utils/placeFiles'
import { navigationTargetLabel, getNavigationTargets, openNavigationTarget } from '../../../../components/Planner/placeNavigation'
import { NavigationMenu } from '../../../../components/shared/NavigationMenu'
import { getAssignmentReservations } from '../../../../utils/dayMerge'
import { getPlaceBookings } from '../../../../utils/dayMerge'
import type { Assignment, Day, Reservation, TripMember } from '../../../../types'
import { ActionCircle, Eyebrow, INNER_CLS } from './MTripSheetUi'
@@ -92,8 +92,9 @@ export default function MPlaceSheet({ planner, shell }: MTripSheetsProps) {
// The bookings attached to that assignment. The desktop inspector shows this
// strip; the phone sheet never did, so a booking reached from a map marker was
// just as unreachable here, only invisibly so (#2012). All of them, because a
// stop can carry a parking pass next to its tickets (#2201).
const linkedReservations = getAssignmentReservations(planner.reservations, assignmentInDay?.id)
// stop can carry a parking pass next to its tickets (#2201). A hotel also lists
// the stay booked for it, the way back to its booking (#2363).
const linkedReservations = getPlaceBookings(planner.reservations, assignmentInDay?.id, place?.id)
// A ferry or a flight has its own form — the reservation modal cannot hold one.
// Resolved up front so a user without the matching right gets no button at all,
// rather than one that does nothing (#2012).
+23 -1
View File
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest'
import { parseTimeToMinutes, getSpanPhase, hidesOnMiddleDay, getTransportRouteEndpoints, getDisplayTimeForDay, getTransportForDay, getAssignmentReservations, getMergedItems, rideSeatKey, stayStartingOn, storedRideSlot, timedSlot } from './dayMerge'
import { parseTimeToMinutes, getSpanPhase, hidesOnMiddleDay, getTransportRouteEndpoints, getDisplayTimeForDay, getTransportForDay, getAssignmentReservations, getPlaceBookings, getMergedItems, rideSeatKey, stayStartingOn, storedRideSlot, timedSlot } from './dayMerge'
describe('parseTimeToMinutes', () => {
it('parses HH:MM string', () => {
@@ -199,6 +199,28 @@ describe('getTransportForDay', () => {
})
})
describe('getPlaceBookings (#2363)', () => {
const reservations = [
{ id: 1, assignment_id: 42, reservation_time: '2025-06-01T10:00:00' },
{ id: 2, assignment_id: null, reservation_time: '2025-06-01T15:00:00', accommodation_place_id: 9 },
{ id: 3, assignment_id: 42, reservation_time: '2025-06-01T09:00:00', accommodation_place_id: 9 },
{ id: 4, assignment_id: null, reservation_time: null, accommodation_place_id: 8 },
]
it('adds the stay booked for a hotel after the bookings pinned to the stop, each once', () => {
expect(getPlaceBookings(reservations, 42, 9).map(r => r.id)).toEqual([3, 1, 2])
})
it('finds the stay even when no stop is open, as from the pill in the day head', () => {
expect(getPlaceBookings(reservations, null, 9).map(r => r.id)).toEqual([2, 3])
})
it('without a place it is the pinned list alone', () => {
expect(getPlaceBookings(reservations, 42, null).map(r => r.id)).toEqual([3, 1])
expect(getPlaceBookings(reservations, null, 7)).toEqual([])
})
})
describe('getAssignmentReservations', () => {
it('returns every booking pinned to the assignment, not just the first (#2201)', () => {
const reservations = [
+19
View File
@@ -267,6 +267,25 @@ export function getAssignmentReservations<T extends {
})
}
/**
* The bookings that belong to a place: the ones pinned to the open stop, and the
* stay booked for it when the place is a hotel (#2363). Without the second, a
* hotel opened from its pill in the day head showed the place but no route to its
* booking. Pinned ones keep their order and come first.
*/
export function getPlaceBookings<T extends {
id: number
assignment_id?: number | null
reservation_time?: string | null
accommodation_place_id?: number | null
}>(reservations: T[], assignmentId: number | null | undefined, placeId: number | null | undefined): T[] {
const pinned = getAssignmentReservations(reservations, assignmentId)
if (placeId == null) return pinned
const pinnedIds = new Set(pinned.map(r => r.id))
const stays = reservations.filter(r => r.accommodation_place_id === placeId && !pinnedIds.has(r.id))
return [...pinned, ...stays]
}
/**
* Order items chronologically: anything with a time (a place's place_time, a
* transport/leg display time, a timed note) sorts by that time. An item WITHOUT a
+1
View File
@@ -19,6 +19,7 @@ const day: TranslationStrings = {
'day.checkIn': 'تسجيل الوصول',
'day.checkInUntil': 'حتى',
'day.checkOut': 'تسجيل المغادرة',
'day.openStayBooking': 'فتح الحجز',
'day.confirmation': 'التأكيد',
'day.editAccommodation': 'تعديل الإقامة',
'day.reservations': 'الحجوزات',
+1
View File
@@ -23,6 +23,7 @@ const day: TranslationStrings = {
'day.checkIn': 'Giriş',
'day.checkInUntil': 'Qədər',
'day.checkOut': 'Çıxış',
'day.openStayBooking': 'Rezervasiyanı aç',
'day.confirmation': 'Təsdiq',
'day.editAccommodation': 'Yaşayış yerini redaktə et',
'day.reservations': 'Rezervasiyalar',
+1
View File
@@ -19,6 +19,7 @@ const day: TranslationStrings = {
'day.checkIn': 'Check-in',
'day.checkInUntil': 'Até',
'day.checkOut': 'Check-out',
'day.openStayBooking': 'Abrir reserva',
'day.confirmation': 'Confirmação',
'day.editAccommodation': 'Editar hospedagem',
'day.reservations': 'Reservas',
+1
View File
@@ -19,6 +19,7 @@ const day: TranslationStrings = {
'day.checkIn': "Registre d'entrada",
'day.checkInUntil': 'Fins a',
'day.checkOut': 'Registre de sortida',
'day.openStayBooking': 'Obre la reserva',
'day.confirmation': 'Confirmació',
'day.editAccommodation': "Edita l'allotjament",
'day.reservations': 'Reserves',
+1
View File
@@ -19,6 +19,7 @@ const day: TranslationStrings = {
'day.checkIn': 'Check-in',
'day.checkInUntil': 'Do',
'day.checkOut': 'Check-out',
'day.openStayBooking': 'Otevřít rezervaci',
'day.confirmation': 'Potvrzení',
'day.editAccommodation': 'Upravit ubytování',
'day.reservations': 'Rezervace',
+1
View File
@@ -20,6 +20,7 @@ const day: TranslationStrings = {
'day.checkIn': 'Check-in',
'day.checkInUntil': 'Bis',
'day.checkOut': 'Check-out',
'day.openStayBooking': 'Buchung öffnen',
'day.confirmation': 'Bestätigung',
'day.editAccommodation': 'Unterkunft bearbeiten',
'day.reservations': 'Reservierungen',
+1
View File
@@ -19,6 +19,7 @@ const day: TranslationStrings = {
'day.checkIn': 'Check-in',
'day.checkInUntil': 'Until',
'day.checkOut': 'Check-out',
'day.openStayBooking': 'Open booking',
'day.confirmation': 'Confirmation',
'day.editAccommodation': 'Edit accommodation',
'day.reservations': 'Reservations',
+1
View File
@@ -20,6 +20,7 @@ const day: TranslationStrings = {
'day.checkIn': 'Registro de entrada',
'day.checkInUntil': 'Hasta',
'day.checkOut': 'Registro de salida',
'day.openStayBooking': 'Abrir reserva',
'day.confirmation': 'Confirmación',
'day.editAccommodation': 'Editar alojamiento',
'day.reservations': 'Reservas',
+1
View File
@@ -19,6 +19,7 @@ const day: TranslationStrings = {
'day.checkIn': 'Sisseregistreerimine',
'day.checkInUntil': 'Kuni',
'day.checkOut': 'Väljaregistreerimine',
'day.openStayBooking': 'Ava broneering',
'day.confirmation': 'Kinnitus',
'day.editAccommodation': 'Muuda majutust',
'day.reservations': 'Broneeringud',
+1
View File
@@ -19,6 +19,7 @@ const day: TranslationStrings = {
'day.checkIn': 'Arrivée',
'day.checkInUntil': "Jusqu'à",
'day.checkOut': 'Départ',
'day.openStayBooking': 'Ouvrir la réservation',
'day.confirmation': 'Confirmation',
'day.editAccommodation': "Modifier l'hébergement",
'day.reservations': 'Réservations',
+1
View File
@@ -20,6 +20,7 @@ const day: TranslationStrings = {
'day.checkIn': 'Check-in',
'day.checkInUntil': 'Έως',
'day.checkOut': 'Check-out',
'day.openStayBooking': 'Άνοιγμα κράτησης',
'day.confirmation': 'Επιβεβαίωση',
'day.editAccommodation': 'Επεξεργασία διαμονής',
'day.reservations': 'Κρατήσεις',
+1
View File
@@ -19,6 +19,7 @@ const day: TranslationStrings = {
'day.checkIn': 'Bejelentkezés',
'day.checkInUntil': 'Eddig',
'day.checkOut': 'Kijelentkezés',
'day.openStayBooking': 'Foglalás megnyitása',
'day.confirmation': 'Visszaigazolás',
'day.editAccommodation': 'Szállás szerkesztése',
'day.reservations': 'Foglalások',
+1
View File
@@ -19,6 +19,7 @@ const day: TranslationStrings = {
'day.checkIn': 'Check-in',
'day.checkInUntil': 'Sampai',
'day.checkOut': 'Check-out',
'day.openStayBooking': 'Buka pemesanan',
'day.confirmation': 'Konfirmasi',
'day.editAccommodation': 'Edit akomodasi',
'day.reservations': 'Reservasi',
+1
View File
@@ -19,6 +19,7 @@ const day: TranslationStrings = {
'day.checkIn': 'Check-in',
'day.checkInUntil': 'Fino a',
'day.checkOut': 'Check-out',
'day.openStayBooking': 'Apri prenotazione',
'day.confirmation': 'Conferma',
'day.editAccommodation': 'Modifica alloggio',
'day.reservations': 'Prenotazioni',
+1
View File
@@ -19,6 +19,7 @@ const day: TranslationStrings = {
'day.checkIn': 'チェックイン',
'day.checkInUntil': 'チェックイン期限',
'day.checkOut': 'チェックアウト',
'day.openStayBooking': '予約を開く',
'day.confirmation': '確認',
'day.editAccommodation': '宿泊先を編集',
'day.reservations': '予約',
+1
View File
@@ -19,6 +19,7 @@ const day: TranslationStrings = {
'day.checkIn': '체크인',
'day.checkInUntil': '까지',
'day.checkOut': '체크아웃',
'day.openStayBooking': '예약 열기',
'day.confirmation': '확인',
'day.editAccommodation': '숙박 편집',
'day.reservations': '예약',
+1
View File
@@ -19,6 +19,7 @@ const day: TranslationStrings = {
'day.checkIn': 'Inchecken',
'day.checkInUntil': 'Tot',
'day.checkOut': 'Uitchecken',
'day.openStayBooking': 'Boeking openen',
'day.confirmation': 'Bevestiging',
'day.editAccommodation': 'Accommodatie bewerken',
'day.reservations': 'Reserveringen',
+1
View File
@@ -19,6 +19,7 @@ const day: TranslationStrings = {
'day.checkIn': 'Zameldowanie',
'day.checkInUntil': 'Do',
'day.checkOut': 'Wymeldowanie',
'day.openStayBooking': 'Otwórz rezerwację',
'day.confirmation': 'Potwierdzenie',
'day.editAccommodation': 'Edytuj zakwaterowanie',
'day.reservations': 'Rezerwacje',
+1
View File
@@ -19,6 +19,7 @@ const day: TranslationStrings = {
'day.checkIn': 'Заезд',
'day.checkInUntil': 'До',
'day.checkOut': 'Выезд',
'day.openStayBooking': 'Открыть бронирование',
'day.confirmation': 'Подтверждение',
'day.editAccommodation': 'Редактировать жильё',
'day.reservations': 'Бронирования',
+1
View File
@@ -19,6 +19,7 @@ const day: TranslationStrings = {
'day.checkIn': 'Check-in',
'day.checkInUntil': 'Do',
'day.checkOut': 'Check-out',
'day.openStayBooking': 'Otvoriť rezerváciu',
'day.confirmation': 'Potvrdenie',
'day.editAccommodation': 'Upraviť ubytovanie',
'day.reservations': 'Rezervácie',
+1
View File
@@ -19,6 +19,7 @@ const day: TranslationStrings = {
'day.checkIn': 'Incheckning',
'day.checkInUntil': 'Tills',
'day.checkOut': 'Utcheckning',
'day.openStayBooking': 'Öppna bokning',
'day.confirmation': 'Bekräftelse',
'day.editAccommodation': 'Redigera boende',
'day.reservations': 'Bokningar',
+1
View File
@@ -19,6 +19,7 @@ const day: TranslationStrings = {
'day.checkIn': "เช็คอิน",
'day.checkInUntil': "เช็กอินได้ถึง",
'day.checkOut': "เช็คเอาท์",
'day.openStayBooking': 'เปิดการจอง',
'day.confirmation': "การยืนยัน",
'day.editAccommodation': "แก้ไขที่พัก",
'day.reservations': "การจอง",
+1
View File
@@ -19,6 +19,7 @@ const day: TranslationStrings = {
'day.checkIn': 'Giriş',
'day.checkInUntil': 'Kadar',
'day.checkOut': 'Çıkış yapmak',
'day.openStayBooking': 'Rezervasyonu aç',
'day.confirmation': 'Onay kodu',
'day.editAccommodation': 'Konaklamayı düzenle',
'day.reservations': 'Rezervasyonlar',
+1
View File
@@ -19,6 +19,7 @@ const day: TranslationStrings = {
'day.checkIn': 'Заїзд',
'day.checkInUntil': 'До',
'day.checkOut': 'Виїзд',
'day.openStayBooking': 'Відкрити бронювання',
'day.confirmation': 'Підтвердження',
'day.editAccommodation': 'Редагувати проживання',
'day.reservations': 'Бронювання',
+1
View File
@@ -19,6 +19,7 @@ const day: TranslationStrings = {
'day.checkIn': 'Nhận phòng',
'day.checkInUntil': 'Cho đến khi',
'day.checkOut': 'Trả phòng',
'day.openStayBooking': 'Mở đặt chỗ',
'day.confirmation': 'Xác nhận',
'day.editAccommodation': 'Chỉnh sửa chỗ ở',
'day.reservations': 'Đặt chỗ',
+1
View File
@@ -19,6 +19,7 @@ const day: TranslationStrings = {
'day.checkIn': '入住',
'day.checkInUntil': '截止',
'day.checkOut': '退房',
'day.openStayBooking': '開啟預訂',
'day.confirmation': '確認號',
'day.editAccommodation': '編輯住宿',
'day.reservations': '預訂',
+1
View File
@@ -19,6 +19,7 @@ const day: TranslationStrings = {
'day.checkIn': '入住',
'day.checkInUntil': '截止',
'day.checkOut': '退房',
'day.openStayBooking': '打开预订',
'day.confirmation': '确认号',
'day.editAccommodation': '编辑住宿',
'day.reservations': '预订',