feat: support check-in time ranges for hotel accommodations

- Add check_in_end column to day_accommodations (Migration 102)
- Server: create/update accommodation accepts check_in_end
- Bidirectional sync: check_in_end synced between accommodation
  and linked reservation metadata (check_in_end_time)
- DayDetailPanel: shows check-in range (e.g. "14:00 – 22:00"),
  new "Until" time picker in hotel form
- ReservationModal: new check-in-until field for hotel bookings
- ReservationsPanel: displays check-in range in metadata cells
- i18n: checkInUntil keys in all 15 languages

Closes #366
This commit is contained in:
Maurice
2026-04-16 00:23:00 +02:00
parent 125436fa87
commit 409a63633c
24 changed files with 84 additions and 27 deletions
@@ -78,7 +78,7 @@ export default function DayDetailPanel({ day, days, places, categories = [], tri
const [showHotelPicker, setShowHotelPicker] = useState(false)
const [hotelDayRange, setHotelDayRange] = useState({ start: day?.id, end: day?.id })
const [hotelCategoryFilter, setHotelCategoryFilter] = useState('')
const [hotelForm, setHotelForm] = useState({ check_in: '', check_out: '', confirmation: '', place_id: null })
const [hotelForm, setHotelForm] = useState({ check_in: '', check_in_end: '', check_out: '', confirmation: '', place_id: null })
useEffect(() => {
if (!day?.date || !lat || !lng) { setWeather(null); return }
@@ -117,6 +117,7 @@ export default function DayDetailPanel({ day, days, places, categories = [], tri
start_day_id: hotelDayRange.start,
end_day_id: hotelDayRange.end,
check_in: hotelForm.check_in || null,
check_in_end: hotelForm.check_in_end || null,
check_out: hotelForm.check_out || null,
confirmation: hotelForm.confirmation || null,
})
@@ -128,7 +129,7 @@ export default function DayDetailPanel({ day, days, places, categories = [], tri
days.some(d => d.id >= a.start_day_id && d.id <= a.end_day_id && d.id === day?.id)
))
setShowHotelPicker(false)
setHotelForm({ check_in: '', check_out: '', confirmation: '', place_id: null })
setHotelForm({ check_in: '', check_in_end: '', check_out: '', confirmation: '', place_id: null })
onAccommodationChange?.()
} catch {}
}
@@ -356,7 +357,7 @@ export default function DayDetailPanel({ day, days, places, categories = [], tri
<div style={{ fontSize: 13, fontWeight: 600, color: 'var(--text-primary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{acc.place_name}</div>
{acc.place_address && <div style={{ fontSize: 10, color: 'var(--text-faint)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{acc.place_address}</div>}
</div>
{canEditDays && <button onClick={() => { setAccommodation(acc); setHotelForm({ check_in: acc.check_in || '', check_out: acc.check_out || '', confirmation: acc.confirmation || '', place_id: acc.place_id }); setHotelDayRange({ start: acc.start_day_id, end: acc.end_day_id }); setShowHotelPicker('edit') }}
{canEditDays && <button onClick={() => { setAccommodation(acc); setHotelForm({ check_in: acc.check_in || '', check_in_end: acc.check_in_end || '', check_out: acc.check_out || '', confirmation: acc.confirmation || '', place_id: acc.place_id }); setHotelDayRange({ start: acc.start_day_id, end: acc.end_day_id }); setShowHotelPicker('edit') }}
style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 3, flexShrink: 0 }}>
<Pencil size={12} style={{ color: 'var(--text-faint)' }} />
</button>}
@@ -368,7 +369,9 @@ export default function DayDetailPanel({ day, days, places, categories = [], tri
<div style={{ display: 'flex', gap: 0, margin: '0 12px 8px', borderRadius: 10, overflow: 'hidden', border: '1px solid var(--border-faint)' }}>
{acc.check_in && (
<div style={{ flex: 1, padding: '8px 10px', borderRight: '1px solid var(--border-faint)', textAlign: 'center' }}>
<div style={{ fontSize: 12, fontWeight: 700, color: 'var(--text-primary)', lineHeight: 1.2 }}>{fmtTime(acc.check_in)}</div>
<div style={{ fontSize: 12, fontWeight: 700, color: 'var(--text-primary)', lineHeight: 1.2 }}>
{fmtTime(acc.check_in)}{acc.check_in_end ? ` ${fmtTime(acc.check_in_end)}` : ''}
</div>
<div style={{ fontSize: 9, color: 'var(--text-faint)', fontWeight: 500, marginTop: 2, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 3 }}>
<LogIn size={8} /> {t('day.checkIn')}
</div>
@@ -488,11 +491,15 @@ export default function DayDetailPanel({ day, days, places, categories = [], tri
{/* Check-in / Check-out / Confirmation */}
<div style={{ padding: '10px 18px', borderBottom: '1px solid var(--border-faint)', display: 'flex', gap: 8, flexWrap: 'wrap' }}>
<div style={{ flex: 1, minWidth: 100 }}>
<div style={{ flex: 1, minWidth: 80 }}>
<label style={{ fontSize: 9, fontWeight: 600, color: 'var(--text-faint)', textTransform: 'uppercase', letterSpacing: '0.04em', display: 'block', marginBottom: 3 }}>{t('day.checkIn')}</label>
<CustomTimePicker value={hotelForm.check_in} onChange={v => setHotelForm(f => ({ ...f, check_in: v }))} placeholder="14:00" />
</div>
<div style={{ flex: 1, minWidth: 100 }}>
<div style={{ flex: 1, minWidth: 80 }}>
<label style={{ fontSize: 9, fontWeight: 600, color: 'var(--text-faint)', textTransform: 'uppercase', letterSpacing: '0.04em', display: 'block', marginBottom: 3 }}>{t('day.checkInUntil')}</label>
<CustomTimePicker value={hotelForm.check_in_end} onChange={v => setHotelForm(f => ({ ...f, check_in_end: v }))} placeholder="22:00" />
</div>
<div style={{ flex: 1, minWidth: 80 }}>
<label style={{ fontSize: 9, fontWeight: 600, color: 'var(--text-faint)', textTransform: 'uppercase', letterSpacing: '0.04em', display: 'block', marginBottom: 3 }}>{t('day.checkOut')}</label>
<CustomTimePicker value={hotelForm.check_out} onChange={v => setHotelForm(f => ({ ...f, check_out: v }))} placeholder="11:00" />
</div>
@@ -570,11 +577,12 @@ export default function DayDetailPanel({ day, days, places, categories = [], tri
start_day_id: hotelDayRange.start,
end_day_id: hotelDayRange.end,
check_in: hotelForm.check_in || null,
check_in_end: hotelForm.check_in_end || null,
check_out: hotelForm.check_out || null,
confirmation: hotelForm.confirmation || null,
})
setShowHotelPicker(false)
setHotelForm({ check_in: '', check_out: '', confirmation: '', place_id: null })
setHotelForm({ check_in: '', check_in_end: '', check_out: '', confirmation: '', place_id: null })
// Reload
accommodationsApi.list(tripId).then(d => {
const all = d.accommodations || []
@@ -89,7 +89,7 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p
meta_airline: '', meta_flight_number: '', meta_departure_airport: '', meta_arrival_airport: '',
meta_departure_timezone: '', meta_arrival_timezone: '',
meta_train_number: '', meta_platform: '', meta_seat: '',
meta_check_in_time: '', meta_check_out_time: '',
meta_check_in_time: '', meta_check_in_end_time: '', meta_check_out_time: '',
hotel_place_id: '', hotel_start_day: '', hotel_end_day: '',
})
const [isSaving, setIsSaving] = useState(false)
@@ -140,6 +140,7 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p
meta_platform: meta.platform || '',
meta_seat: meta.seat || '',
meta_check_in_time: meta.check_in_time || '',
meta_check_in_end_time: meta.check_in_end_time || '',
meta_check_out_time: meta.check_out_time || '',
hotel_place_id: (() => { const acc = accommodations.find(a => a.id == reservation.accommodation_id); return acc?.place_id || '' })(),
hotel_start_day: (() => { const acc = accommodations.find(a => a.id == reservation.accommodation_id); return acc?.start_day_id || '' })(),
@@ -156,7 +157,7 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p
meta_airline: '', meta_flight_number: '', meta_departure_airport: '', meta_arrival_airport: '',
meta_departure_timezone: '', meta_arrival_timezone: '',
meta_train_number: '', meta_platform: '', meta_seat: '',
meta_check_in_time: '', meta_check_out_time: '',
meta_check_in_time: '', meta_check_in_end_time: '', meta_check_out_time: '',
})
setPendingFiles([])
}
@@ -207,6 +208,7 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p
if (form.meta_arrival_timezone) metadata.arrival_timezone = form.meta_arrival_timezone
} else if (form.type === 'hotel') {
if (form.meta_check_in_time) metadata.check_in_time = form.meta_check_in_time
if (form.meta_check_in_end_time) metadata.check_in_end_time = form.meta_check_in_end_time
if (form.meta_check_out_time) metadata.check_out_time = form.meta_check_out_time
} else if (form.type === 'train') {
if (form.meta_train_number) metadata.train_number = form.meta_train_number
@@ -245,6 +247,7 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p
start_day_id: form.hotel_start_day,
end_day_id: form.hotel_end_day,
check_in: form.meta_check_in_time || null,
check_in_end: form.meta_check_in_end_time || null,
check_out: form.meta_check_out_time || null,
confirmation: form.confirmation_number || null,
}
@@ -526,11 +529,15 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p
</div>
</div>
{/* Check-in/out times + Status */}
<div className="grid grid-cols-3 gap-3">
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
<div>
<label style={labelStyle}>{t('reservations.meta.checkIn')}</label>
<CustomTimePicker value={form.meta_check_in_time} onChange={v => set('meta_check_in_time', v)} />
</div>
<div>
<label style={labelStyle}>{t('reservations.meta.checkInUntil')}</label>
<CustomTimePicker value={form.meta_check_in_end_time} onChange={v => set('meta_check_in_end_time', v)} />
</div>
<div>
<label style={labelStyle}>{t('reservations.meta.checkOut')}</label>
<CustomTimePicker value={form.meta_check_out_time} onChange={v => set('meta_check_out_time', v)} />
@@ -230,7 +230,7 @@ function ReservationCard({ r, tripId, onEdit, onDelete, files = [], onNavigateTo
if (meta.train_number) cells.push({ label: t('reservations.meta.trainNumber'), value: meta.train_number })
if (meta.platform) cells.push({ label: t('reservations.meta.platform'), value: meta.platform })
if (meta.seat) cells.push({ label: t('reservations.meta.seat'), value: meta.seat })
if (meta.check_in_time) cells.push({ label: t('reservations.meta.checkIn'), value: fmtTime('2000-01-01T' + meta.check_in_time) })
if (meta.check_in_time) cells.push({ label: t('reservations.meta.checkIn'), value: fmtTime('2000-01-01T' + meta.check_in_time) + (meta.check_in_end_time ? ` ${fmtTime('2000-01-01T' + meta.check_in_end_time)}` : '') })
if (meta.check_out_time) cells.push({ label: t('reservations.meta.checkOut'), value: fmtTime('2000-01-01T' + meta.check_out_time) })
if (cells.length === 0) return null
return (
+2
View File
@@ -1015,6 +1015,7 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
'reservations.meta.platform': 'المنصة',
'reservations.meta.seat': 'المقعد',
'reservations.meta.checkIn': 'تسجيل الوصول',
'reservations.meta.checkInUntil': 'تسجيل الدخول حتى',
'reservations.meta.checkOut': 'تسجيل المغادرة',
'reservations.meta.linkAccommodation': 'الإقامة',
'reservations.meta.pickAccommodation': 'ربط بالإقامة',
@@ -1499,6 +1500,7 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
'day.noPlacesForHotel': 'أضف أماكن إلى رحلتك أولًا',
'day.allDays': 'الكل',
'day.checkIn': 'تسجيل الوصول',
'day.checkInUntil': 'حتى',
'day.checkOut': 'تسجيل المغادرة',
'day.confirmation': 'التأكيد',
'day.editAccommodation': 'تعديل الإقامة',
+2
View File
@@ -984,6 +984,7 @@ const br: Record<string, string | { name: string; category: string }[]> = {
'reservations.meta.platform': 'Plataforma',
'reservations.meta.seat': 'Assento',
'reservations.meta.checkIn': 'Check-in',
'reservations.meta.checkInUntil': 'Check-in até',
'reservations.meta.checkOut': 'Check-out',
'reservations.meta.linkAccommodation': 'Hospedagem',
'reservations.meta.pickAccommodation': 'Vincular à hospedagem',
@@ -1468,6 +1469,7 @@ const br: Record<string, string | { name: string; category: string }[]> = {
'day.noPlacesForHotel': 'Adicione lugares à viagem primeiro',
'day.allDays': 'Todos',
'day.checkIn': 'Check-in',
'day.checkInUntil': 'Até',
'day.checkOut': 'Check-out',
'day.confirmation': 'Confirmação',
'day.editAccommodation': 'Editar hospedagem',
+2
View File
@@ -1013,6 +1013,7 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
'reservations.meta.platform': 'Nástupiště',
'reservations.meta.seat': 'Sedadlo',
'reservations.meta.checkIn': 'Check-in',
'reservations.meta.checkInUntil': 'Check-in do',
'reservations.meta.checkOut': 'Check-out',
'reservations.meta.linkAccommodation': 'Ubytování',
'reservations.meta.pickAccommodation': 'Propojit s ubytováním',
@@ -1497,6 +1498,7 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
'day.noPlacesForHotel': 'Nejprve přidejte místa ke své cestě',
'day.allDays': 'Vše',
'day.checkIn': 'Check-in',
'day.checkInUntil': 'Do',
'day.checkOut': 'Check-out',
'day.confirmation': 'Potvrzení',
'day.editAccommodation': 'Upravit ubytování',
+2
View File
@@ -1015,6 +1015,7 @@ const de: Record<string, string | { name: string; category: string }[]> = {
'reservations.meta.platform': 'Gleis',
'reservations.meta.seat': 'Sitzplatz',
'reservations.meta.checkIn': 'Check-in',
'reservations.meta.checkInUntil': 'Check-in bis',
'reservations.meta.checkOut': 'Check-out',
'reservations.meta.linkAccommodation': 'Unterkunft',
'reservations.meta.pickAccommodation': 'Mit Unterkunft verknüpfen',
@@ -1499,6 +1500,7 @@ const de: Record<string, string | { name: string; category: string }[]> = {
'day.noPlacesForHotel': 'Füge zuerst Orte zu deiner Reise hinzu',
'day.allDays': 'Alle',
'day.checkIn': 'Check-in',
'day.checkInUntil': 'Bis',
'day.checkOut': 'Check-out',
'day.confirmation': 'Bestätigung',
'day.editAccommodation': 'Unterkunft bearbeiten',
+2
View File
@@ -1068,6 +1068,7 @@ const en: Record<string, string | { name: string; category: string }[]> = {
'reservations.meta.platform': 'Platform',
'reservations.meta.seat': 'Seat',
'reservations.meta.checkIn': 'Check-in',
'reservations.meta.checkInUntil': 'Check-in until',
'reservations.meta.checkOut': 'Check-out',
'reservations.meta.linkAccommodation': 'Accommodation',
'reservations.meta.pickAccommodation': 'Link to accommodation',
@@ -1552,6 +1553,7 @@ const en: Record<string, string | { name: string; category: string }[]> = {
'day.noPlacesForHotel': 'Add places to your trip first',
'day.allDays': 'All',
'day.checkIn': 'Check-in',
'day.checkInUntil': 'Until',
'day.checkOut': 'Check-out',
'day.confirmation': 'Confirmation',
'day.editAccommodation': 'Edit accommodation',
+2
View File
@@ -1448,6 +1448,7 @@ const es: Record<string, string> = {
'day.noPlacesForHotel': 'Añade primero lugares al viaje',
'day.allDays': 'Todos',
'day.checkIn': 'Registro de entrada',
'day.checkInUntil': 'Hasta',
'day.checkOut': 'Registro de salida',
'day.confirmation': 'Confirmación',
'day.editAccommodation': 'Editar alojamiento',
@@ -1615,6 +1616,7 @@ const es: Record<string, string> = {
'reservations.meta.platform': 'Andén',
'reservations.meta.seat': 'Asiento',
'reservations.meta.checkIn': 'Registro de entrada',
'reservations.meta.checkInUntil': 'Check-in hasta',
'reservations.meta.checkOut': 'Registro de salida',
'reservations.meta.linkAccommodation': 'Alojamiento',
'reservations.meta.pickAccommodation': 'Vincular con alojamiento',
+2
View File
@@ -1011,6 +1011,7 @@ const fr: Record<string, string> = {
'reservations.meta.platform': 'Quai',
'reservations.meta.seat': 'Place',
'reservations.meta.checkIn': 'Arrivée',
'reservations.meta.checkInUntil': "Check-in jusqu'à",
'reservations.meta.checkOut': 'Départ',
'reservations.meta.linkAccommodation': 'Hébergement',
'reservations.meta.pickAccommodation': 'Lier à un hébergement',
@@ -1495,6 +1496,7 @@ const fr: Record<string, string> = {
'day.noPlacesForHotel': 'Ajoutez d\'abord des lieux à votre voyage',
'day.allDays': 'Tous',
'day.checkIn': 'Arrivée',
'day.checkInUntil': "Jusqu'à",
'day.checkOut': 'Départ',
'day.confirmation': 'Confirmation',
'day.editAccommodation': 'Modifier l\'hébergement',
+2
View File
@@ -1013,6 +1013,7 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
'reservations.meta.platform': 'Vágány',
'reservations.meta.seat': 'Ülés',
'reservations.meta.checkIn': 'Bejelentkezés',
'reservations.meta.checkInUntil': 'Bejelentkezés eddig',
'reservations.meta.checkOut': 'Kijelentkezés',
'reservations.meta.linkAccommodation': 'Szállás',
'reservations.meta.pickAccommodation': 'Szállás hozzárendelése',
@@ -1496,6 +1497,7 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
'day.noPlacesForHotel': 'Először adj hozzá helyeket az utazásodhoz',
'day.allDays': 'Összes',
'day.checkIn': 'Bejelentkezés',
'day.checkInUntil': 'Eddig',
'day.checkOut': 'Kijelentkezés',
'day.confirmation': 'Visszaigazolás',
'day.editAccommodation': 'Szállás szerkesztése',
+2
View File
@@ -1068,6 +1068,7 @@ const id: Record<string, string | { name: string; category: string }[]> = {
'reservations.meta.platform': 'Peron',
'reservations.meta.seat': 'Kursi',
'reservations.meta.checkIn': 'Check-in',
'reservations.meta.checkInUntil': 'Check-in sampai',
'reservations.meta.checkOut': 'Check-out',
'reservations.meta.linkAccommodation': 'Akomodasi',
'reservations.meta.pickAccommodation': 'Hubungkan ke akomodasi',
@@ -1552,6 +1553,7 @@ const id: Record<string, string | { name: string; category: string }[]> = {
'day.noPlacesForHotel': 'Tambahkan tempat ke perjalananmu terlebih dahulu',
'day.allDays': 'Semua',
'day.checkIn': 'Check-in',
'day.checkInUntil': 'Sampai',
'day.checkOut': 'Check-out',
'day.confirmation': 'Konfirmasi',
'day.editAccommodation': 'Edit akomodasi',
+2
View File
@@ -1012,6 +1012,7 @@ const it: Record<string, string | { name: string; category: string }[]> = {
'reservations.meta.platform': 'Binario',
'reservations.meta.seat': 'Posto',
'reservations.meta.checkIn': 'Check-in',
'reservations.meta.checkInUntil': 'Check-in fino a',
'reservations.meta.checkOut': 'Check-out',
'reservations.meta.linkAccommodation': 'Alloggio',
'reservations.meta.pickAccommodation': 'Collega a un alloggio',
@@ -1496,6 +1497,7 @@ const it: Record<string, string | { name: string; category: string }[]> = {
'day.noPlacesForHotel': 'Aggiungi prima i luoghi al tuo viaggio',
'day.allDays': 'Tutti',
'day.checkIn': 'Check-in',
'day.checkInUntil': 'Fino a',
'day.checkOut': 'Check-out',
'day.confirmation': 'Conferma',
'day.editAccommodation': 'Modifica alloggio',
+2
View File
@@ -1011,6 +1011,7 @@ const nl: Record<string, string> = {
'reservations.meta.platform': 'Perron',
'reservations.meta.seat': 'Stoel',
'reservations.meta.checkIn': 'Inchecken',
'reservations.meta.checkInUntil': 'Check-in tot',
'reservations.meta.checkOut': 'Uitchecken',
'reservations.meta.linkAccommodation': 'Accommodatie',
'reservations.meta.pickAccommodation': 'Koppel aan accommodatie',
@@ -1495,6 +1496,7 @@ const nl: Record<string, string> = {
'day.noPlacesForHotel': 'Voeg eerst plaatsen toe aan je reis',
'day.allDays': 'Alle',
'day.checkIn': 'Inchecken',
'day.checkInUntil': 'Tot',
'day.checkOut': 'Uitchecken',
'day.confirmation': 'Bevestiging',
'day.editAccommodation': 'Accommodatie bewerken',
+2
View File
@@ -968,6 +968,7 @@ const pl: Record<string, string | { name: string; category: string }[]> = {
'reservations.meta.platform': 'Peron',
'reservations.meta.seat': 'Miejsce',
'reservations.meta.checkIn': 'Zameldowanie',
'reservations.meta.checkInUntil': 'Check-in do',
'reservations.meta.checkOut': 'Wymeldowanie',
'reservations.meta.linkAccommodation': 'Zakwaterowanie',
'reservations.meta.pickAccommodation': 'Link do zakwaterowania',
@@ -1450,6 +1451,7 @@ const pl: Record<string, string | { name: string; category: string }[]> = {
'day.noPlacesForHotel': 'Najpierw dodaj miejsca do swojej podróży',
'day.allDays': 'Wszystkie',
'day.checkIn': 'Zameldowanie',
'day.checkInUntil': 'Do',
'day.checkOut': 'Wymeldowanie',
'day.confirmation': 'Potwierdzenie',
'day.editAccommodation': 'Edytuj zakwaterowanie',
+2
View File
@@ -1011,6 +1011,7 @@ const ru: Record<string, string> = {
'reservations.meta.platform': 'Платформа',
'reservations.meta.seat': 'Место',
'reservations.meta.checkIn': 'Заезд',
'reservations.meta.checkInUntil': 'Заселение до',
'reservations.meta.checkOut': 'Выезд',
'reservations.meta.linkAccommodation': 'Жильё',
'reservations.meta.pickAccommodation': 'Привязать к жилью',
@@ -1495,6 +1496,7 @@ const ru: Record<string, string> = {
'day.noPlacesForHotel': 'Сначала добавьте места в поездку',
'day.allDays': 'Все',
'day.checkIn': 'Заезд',
'day.checkInUntil': 'До',
'day.checkOut': 'Выезд',
'day.confirmation': 'Подтверждение',
'day.editAccommodation': 'Редактировать жильё',
+2
View File
@@ -1011,6 +1011,7 @@ const zh: Record<string, string> = {
'reservations.meta.platform': '站台',
'reservations.meta.seat': '座位',
'reservations.meta.checkIn': '入住',
'reservations.meta.checkInUntil': '入住截止',
'reservations.meta.checkOut': '退房',
'reservations.meta.linkAccommodation': '住宿',
'reservations.meta.pickAccommodation': '关联住宿',
@@ -1495,6 +1496,7 @@ const zh: Record<string, string> = {
'day.noPlacesForHotel': '请先在旅行中添加地点',
'day.allDays': '全部',
'day.checkIn': '入住',
'day.checkInUntil': '截止',
'day.checkOut': '退房',
'day.confirmation': '确认号',
'day.editAccommodation': '编辑住宿',
+2
View File
@@ -1067,6 +1067,7 @@ const zhTw: Record<string, string> = {
'reservations.meta.platform': '站臺',
'reservations.meta.seat': '座位',
'reservations.meta.checkIn': '入住',
'reservations.meta.checkInUntil': '入住截止',
'reservations.meta.checkOut': '退房',
'reservations.meta.linkAccommodation': '住宿',
'reservations.meta.pickAccommodation': '關聯住宿',
@@ -1551,6 +1552,7 @@ const zhTw: Record<string, string> = {
'day.noPlacesForHotel': '請先在旅行中新增地點',
'day.allDays': '全部',
'day.checkIn': '入住',
'day.checkInUntil': '截止',
'day.checkOut': '退房',
'day.confirmation': '確認號',
'day.editAccommodation': '編輯住宿',
+1
View File
@@ -241,6 +241,7 @@ export interface Accommodation {
name: string
address: string | null
check_in: string | null
check_in_end: string | null
check_out: string | null
confirmation_number: string | null
notes: string | null
+5
View File
@@ -1610,6 +1610,11 @@ function runMigrations(db: Database.Database): void {
() => {
db.prepare("UPDATE addons SET enabled = 1 WHERE id = 'naver_list_import'").run();
},
// Migration 102: Add check_in_end column for check-in time ranges
() => {
try { db.exec('ALTER TABLE day_accommodations ADD COLUMN check_in_end TEXT'); } catch (err: any) { if (!err.message?.includes('duplicate column name')) throw err; }
},
];
if (currentVersion < migrations.length) {
+1
View File
@@ -334,6 +334,7 @@ function createTables(db: Database.Database): void {
start_day_id INTEGER NOT NULL REFERENCES days(id) ON DELETE CASCADE,
end_day_id INTEGER NOT NULL REFERENCES days(id) ON DELETE CASCADE,
check_in TEXT,
check_in_end TEXT,
check_out TEXT,
confirmation TEXT,
notes TEXT,
+4 -4
View File
@@ -73,7 +73,7 @@ accommodationsRouter.post('/', authenticate, requireTripAccess, (req: Request, r
return res.status(403).json({ error: 'No permission' });
const { tripId } = req.params;
const { place_id, start_day_id, end_day_id, check_in, check_out, confirmation, notes } = req.body;
const { place_id, start_day_id, end_day_id, check_in, check_in_end, check_out, confirmation, notes } = req.body;
if (!place_id || !start_day_id || !end_day_id) {
return res.status(400).json({ error: 'place_id, start_day_id, and end_day_id are required' });
@@ -82,7 +82,7 @@ accommodationsRouter.post('/', authenticate, requireTripAccess, (req: Request, r
const errors = dayService.validateAccommodationRefs(tripId, place_id, start_day_id, end_day_id);
if (errors.length > 0) return res.status(404).json({ error: errors[0].message });
const accommodation = dayService.createAccommodation(tripId, { place_id, start_day_id, end_day_id, check_in, check_out, confirmation, notes });
const accommodation = dayService.createAccommodation(tripId, { place_id, start_day_id, end_day_id, check_in, check_in_end, check_out, confirmation, notes });
res.status(201).json({ accommodation });
broadcast(tripId, 'accommodation:created', { accommodation }, req.headers['x-socket-id'] as string);
broadcast(tripId, 'reservation:created', {}, req.headers['x-socket-id'] as string);
@@ -98,12 +98,12 @@ accommodationsRouter.put('/:id', authenticate, requireTripAccess, (req: Request,
const existing = dayService.getAccommodation(id, tripId);
if (!existing) return res.status(404).json({ error: 'Accommodation not found' });
const { place_id, start_day_id, end_day_id, check_in, check_out, confirmation, notes } = req.body;
const { place_id, start_day_id, end_day_id, check_in, check_in_end, check_out, confirmation, notes } = req.body;
const errors = dayService.validateAccommodationRefs(tripId, place_id, start_day_id, end_day_id);
if (errors.length > 0) return res.status(404).json({ error: errors[0].message });
const accommodation = dayService.updateAccommodation(id, existing, { place_id, start_day_id, end_day_id, check_in, check_out, confirmation, notes });
const accommodation = dayService.updateAccommodation(id, existing, { place_id, start_day_id, end_day_id, check_in, check_in_end, check_out, confirmation, notes });
res.json({ accommodation });
broadcast(tripId, 'accommodation:updated', { accommodation }, req.headers['x-socket-id'] as string);
});
+11 -6
View File
@@ -170,6 +170,7 @@ export interface DayAccommodation {
start_day_id: number;
end_day_id: number;
check_in: string | null;
check_in_end: string | null;
check_out: string | null;
confirmation: string | null;
notes: string | null;
@@ -220,17 +221,18 @@ interface CreateAccommodationData {
start_day_id: number;
end_day_id: number;
check_in?: string;
check_in_end?: string;
check_out?: string;
confirmation?: string;
notes?: string;
}
export function createAccommodation(tripId: string | number, data: CreateAccommodationData) {
const { place_id, start_day_id, end_day_id, check_in, check_out, confirmation, notes } = data;
const { place_id, start_day_id, end_day_id, check_in, check_in_end, check_out, confirmation, notes } = data;
const result = db.prepare(
'INSERT INTO day_accommodations (trip_id, place_id, start_day_id, end_day_id, check_in, check_out, confirmation, notes) VALUES (?, ?, ?, ?, ?, ?, ?, ?)'
).run(tripId, place_id, start_day_id, end_day_id, check_in || null, check_out || null, confirmation || null, notes || null);
'INSERT INTO day_accommodations (trip_id, place_id, start_day_id, end_day_id, check_in, check_in_end, check_out, confirmation, notes) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)'
).run(tripId, place_id, start_day_id, end_day_id, check_in || null, check_in_end || null, check_out || null, confirmation || null, notes || null);
const accommodationId = result.lastInsertRowid;
@@ -239,6 +241,7 @@ export function createAccommodation(tripId: string | number, data: CreateAccommo
const startDayDate = (db.prepare('SELECT date FROM days WHERE id = ?').get(start_day_id) as { date: string } | undefined)?.date || null;
const meta: Record<string, string> = {};
if (check_in) meta.check_in_time = check_in;
if (check_in_end) meta.check_in_end_time = check_in_end;
if (check_out) meta.check_out_time = check_out;
db.prepare(`
INSERT INTO reservations (trip_id, day_id, title, reservation_time, location, confirmation_number, notes, status, type, accommodation_id, metadata)
@@ -258,25 +261,27 @@ export function getAccommodation(id: string | number, tripId: string | number) {
export function updateAccommodation(id: string | number, existing: DayAccommodation, fields: {
place_id?: number; start_day_id?: number; end_day_id?: number;
check_in?: string; check_out?: string; confirmation?: string; notes?: string;
check_in?: string; check_in_end?: string; check_out?: string; confirmation?: string; notes?: string;
}) {
const newPlaceId = fields.place_id !== undefined ? fields.place_id : existing.place_id;
const newStartDayId = fields.start_day_id !== undefined ? fields.start_day_id : existing.start_day_id;
const newEndDayId = fields.end_day_id !== undefined ? fields.end_day_id : existing.end_day_id;
const newCheckIn = fields.check_in !== undefined ? fields.check_in : existing.check_in;
const newCheckInEnd = fields.check_in_end !== undefined ? fields.check_in_end : existing.check_in_end;
const newCheckOut = fields.check_out !== undefined ? fields.check_out : existing.check_out;
const newConfirmation = fields.confirmation !== undefined ? fields.confirmation : existing.confirmation;
const newNotes = fields.notes !== undefined ? fields.notes : existing.notes;
db.prepare(
'UPDATE day_accommodations SET place_id = ?, start_day_id = ?, end_day_id = ?, check_in = ?, check_out = ?, confirmation = ?, notes = ? WHERE id = ?'
).run(newPlaceId, newStartDayId, newEndDayId, newCheckIn, newCheckOut, newConfirmation, newNotes, id);
'UPDATE day_accommodations SET place_id = ?, start_day_id = ?, end_day_id = ?, check_in = ?, check_in_end = ?, check_out = ?, confirmation = ?, notes = ? WHERE id = ?'
).run(newPlaceId, newStartDayId, newEndDayId, newCheckIn, newCheckInEnd, newCheckOut, newConfirmation, newNotes, id);
// Sync check-in/out/confirmation to linked reservation
const linkedRes = db.prepare('SELECT id, metadata FROM reservations WHERE accommodation_id = ?').get(Number(id)) as { id: number; metadata: string | null } | undefined;
if (linkedRes) {
const meta = linkedRes.metadata ? JSON.parse(linkedRes.metadata) : {};
if (newCheckIn) meta.check_in_time = newCheckIn;
if (newCheckInEnd) meta.check_in_end_time = newCheckInEnd;
if (newCheckOut) meta.check_out_time = newCheckOut;
db.prepare('UPDATE reservations SET metadata = ?, confirmation_number = COALESCE(?, confirmation_number) WHERE id = ?')
.run(JSON.stringify(meta), newConfirmation || null, linkedRes.id);
+6 -6
View File
@@ -123,9 +123,9 @@ export function createReservation(tripId: string | number, data: CreateReservati
// Sync check-in/out to accommodation if linked
if (accommodation_id && metadata) {
const meta = typeof metadata === 'string' ? JSON.parse(metadata) : metadata;
if (meta.check_in_time || meta.check_out_time) {
db.prepare('UPDATE day_accommodations SET check_in = COALESCE(?, check_in), check_out = COALESCE(?, check_out) WHERE id = ?')
.run(meta.check_in_time || null, meta.check_out_time || null, accommodation_id);
if (meta.check_in_time || meta.check_in_end_time || meta.check_out_time) {
db.prepare('UPDATE day_accommodations SET check_in = COALESCE(?, check_in), check_in_end = COALESCE(?, check_in_end), check_out = COALESCE(?, check_out) WHERE id = ?')
.run(meta.check_in_time || null, meta.check_in_end_time || null, meta.check_out_time || null, accommodation_id);
}
if (confirmation_number) {
db.prepare('UPDATE day_accommodations SET confirmation = COALESCE(?, confirmation) WHERE id = ?')
@@ -257,9 +257,9 @@ export function updateReservation(id: string | number, tripId: string | number,
const resolvedMeta = metadata !== undefined ? metadata : (current.metadata ? JSON.parse(current.metadata as string) : null);
if (resolvedAccId && resolvedMeta) {
const meta = typeof resolvedMeta === 'string' ? JSON.parse(resolvedMeta) : resolvedMeta;
if (meta.check_in_time || meta.check_out_time) {
db.prepare('UPDATE day_accommodations SET check_in = COALESCE(?, check_in), check_out = COALESCE(?, check_out) WHERE id = ?')
.run(meta.check_in_time || null, meta.check_out_time || null, resolvedAccId);
if (meta.check_in_time || meta.check_in_end_time || meta.check_out_time) {
db.prepare('UPDATE day_accommodations SET check_in = COALESCE(?, check_in), check_in_end = COALESCE(?, check_in_end), check_out = COALESCE(?, check_out) WHERE id = ?')
.run(meta.check_in_time || null, meta.check_in_end_time || null, meta.check_out_time || null, resolvedAccId);
}
const resolvedConf = confirmation_number !== undefined ? confirmation_number : current.confirmation_number;
if (resolvedConf) {