From 409a63633c46bbf4abca78b98dbd1e353e7c7ff1 Mon Sep 17 00:00:00 2001 From: Maurice Date: Thu, 16 Apr 2026 00:23:00 +0200 Subject: [PATCH] feat: support check-in time ranges for hotel accommodations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../src/components/Planner/DayDetailPanel.tsx | 22 +++++++++++++------ .../components/Planner/ReservationModal.tsx | 13 ++++++++--- .../components/Planner/ReservationsPanel.tsx | 2 +- client/src/i18n/translations/ar.ts | 2 ++ client/src/i18n/translations/br.ts | 2 ++ client/src/i18n/translations/cs.ts | 2 ++ client/src/i18n/translations/de.ts | 2 ++ client/src/i18n/translations/en.ts | 2 ++ client/src/i18n/translations/es.ts | 2 ++ client/src/i18n/translations/fr.ts | 2 ++ client/src/i18n/translations/hu.ts | 2 ++ client/src/i18n/translations/id.ts | 2 ++ client/src/i18n/translations/it.ts | 2 ++ client/src/i18n/translations/nl.ts | 2 ++ client/src/i18n/translations/pl.ts | 2 ++ client/src/i18n/translations/ru.ts | 2 ++ client/src/i18n/translations/zh.ts | 2 ++ client/src/i18n/translations/zhTw.ts | 2 ++ client/src/types.ts | 1 + server/src/db/migrations.ts | 5 +++++ server/src/db/schema.ts | 1 + server/src/routes/days.ts | 8 +++---- server/src/services/dayService.ts | 17 +++++++++----- server/src/services/reservationService.ts | 12 +++++----- 24 files changed, 84 insertions(+), 27 deletions(-) diff --git a/client/src/components/Planner/DayDetailPanel.tsx b/client/src/components/Planner/DayDetailPanel.tsx index c1a4acc3..3ff8b102 100644 --- a/client/src/components/Planner/DayDetailPanel.tsx +++ b/client/src/components/Planner/DayDetailPanel.tsx @@ -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
{acc.place_name}
{acc.place_address &&
{acc.place_address}
} - {canEditDays && } @@ -368,7 +369,9 @@ export default function DayDetailPanel({ day, days, places, categories = [], tri
{acc.check_in && (
-
{fmtTime(acc.check_in)}
+
+ {fmtTime(acc.check_in)}{acc.check_in_end ? ` – ${fmtTime(acc.check_in_end)}` : ''} +
{t('day.checkIn')}
@@ -488,11 +491,15 @@ export default function DayDetailPanel({ day, days, places, categories = [], tri {/* Check-in / Check-out / Confirmation */}
-
+
setHotelForm(f => ({ ...f, check_in: v }))} placeholder="14:00" />
-
+
+ + setHotelForm(f => ({ ...f, check_in_end: v }))} placeholder="22:00" /> +
+
setHotelForm(f => ({ ...f, check_out: v }))} placeholder="11:00" />
@@ -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 || [] diff --git a/client/src/components/Planner/ReservationModal.tsx b/client/src/components/Planner/ReservationModal.tsx index a70924ed..fb2bb01b 100644 --- a/client/src/components/Planner/ReservationModal.tsx +++ b/client/src/components/Planner/ReservationModal.tsx @@ -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
{/* Check-in/out times + Status */} -
+
set('meta_check_in_time', v)} />
+
+ + set('meta_check_in_end_time', v)} /> +
set('meta_check_out_time', v)} /> diff --git a/client/src/components/Planner/ReservationsPanel.tsx b/client/src/components/Planner/ReservationsPanel.tsx index 399409b1..51f85e5e 100644 --- a/client/src/components/Planner/ReservationsPanel.tsx +++ b/client/src/components/Planner/ReservationsPanel.tsx @@ -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 ( diff --git a/client/src/i18n/translations/ar.ts b/client/src/i18n/translations/ar.ts index 121f431f..1a2e35ce 100644 --- a/client/src/i18n/translations/ar.ts +++ b/client/src/i18n/translations/ar.ts @@ -1015,6 +1015,7 @@ const ar: Record = { '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 = { 'day.noPlacesForHotel': 'أضف أماكن إلى رحلتك أولًا', 'day.allDays': 'الكل', 'day.checkIn': 'تسجيل الوصول', + 'day.checkInUntil': 'حتى', 'day.checkOut': 'تسجيل المغادرة', 'day.confirmation': 'التأكيد', 'day.editAccommodation': 'تعديل الإقامة', diff --git a/client/src/i18n/translations/br.ts b/client/src/i18n/translations/br.ts index 6d5f2363..98f59e05 100644 --- a/client/src/i18n/translations/br.ts +++ b/client/src/i18n/translations/br.ts @@ -984,6 +984,7 @@ const br: Record = { '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 = { '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', diff --git a/client/src/i18n/translations/cs.ts b/client/src/i18n/translations/cs.ts index 0b6bb15f..2b57ef61 100644 --- a/client/src/i18n/translations/cs.ts +++ b/client/src/i18n/translations/cs.ts @@ -1013,6 +1013,7 @@ const cs: Record = { '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 = { '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í', diff --git a/client/src/i18n/translations/de.ts b/client/src/i18n/translations/de.ts index 3f7990be..1547e631 100644 --- a/client/src/i18n/translations/de.ts +++ b/client/src/i18n/translations/de.ts @@ -1015,6 +1015,7 @@ const de: Record = { '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 = { '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', diff --git a/client/src/i18n/translations/en.ts b/client/src/i18n/translations/en.ts index 377b9d6a..c4d5588f 100644 --- a/client/src/i18n/translations/en.ts +++ b/client/src/i18n/translations/en.ts @@ -1068,6 +1068,7 @@ const en: Record = { '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 = { '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', diff --git a/client/src/i18n/translations/es.ts b/client/src/i18n/translations/es.ts index 12afa1c3..061bfa9c 100644 --- a/client/src/i18n/translations/es.ts +++ b/client/src/i18n/translations/es.ts @@ -1448,6 +1448,7 @@ const es: Record = { '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 = { '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', diff --git a/client/src/i18n/translations/fr.ts b/client/src/i18n/translations/fr.ts index bea2019d..2050f551 100644 --- a/client/src/i18n/translations/fr.ts +++ b/client/src/i18n/translations/fr.ts @@ -1011,6 +1011,7 @@ const fr: Record = { '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 = { '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', diff --git a/client/src/i18n/translations/hu.ts b/client/src/i18n/translations/hu.ts index df9930db..3e9083e3 100644 --- a/client/src/i18n/translations/hu.ts +++ b/client/src/i18n/translations/hu.ts @@ -1013,6 +1013,7 @@ const hu: Record = { '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 = { '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', diff --git a/client/src/i18n/translations/id.ts b/client/src/i18n/translations/id.ts index 97ea0e6d..82b3e317 100644 --- a/client/src/i18n/translations/id.ts +++ b/client/src/i18n/translations/id.ts @@ -1068,6 +1068,7 @@ const id: Record = { '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 = { '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', diff --git a/client/src/i18n/translations/it.ts b/client/src/i18n/translations/it.ts index fc0236d1..fc90c6aa 100644 --- a/client/src/i18n/translations/it.ts +++ b/client/src/i18n/translations/it.ts @@ -1012,6 +1012,7 @@ const it: Record = { '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 = { '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', diff --git a/client/src/i18n/translations/nl.ts b/client/src/i18n/translations/nl.ts index 5b601888..f5c26556 100644 --- a/client/src/i18n/translations/nl.ts +++ b/client/src/i18n/translations/nl.ts @@ -1011,6 +1011,7 @@ const nl: Record = { '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 = { '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', diff --git a/client/src/i18n/translations/pl.ts b/client/src/i18n/translations/pl.ts index 38a30885..c8c84921 100644 --- a/client/src/i18n/translations/pl.ts +++ b/client/src/i18n/translations/pl.ts @@ -968,6 +968,7 @@ const pl: Record = { '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 = { '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', diff --git a/client/src/i18n/translations/ru.ts b/client/src/i18n/translations/ru.ts index 693081a3..b3baef9a 100644 --- a/client/src/i18n/translations/ru.ts +++ b/client/src/i18n/translations/ru.ts @@ -1011,6 +1011,7 @@ const ru: Record = { '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 = { 'day.noPlacesForHotel': 'Сначала добавьте места в поездку', 'day.allDays': 'Все', 'day.checkIn': 'Заезд', + 'day.checkInUntil': 'До', 'day.checkOut': 'Выезд', 'day.confirmation': 'Подтверждение', 'day.editAccommodation': 'Редактировать жильё', diff --git a/client/src/i18n/translations/zh.ts b/client/src/i18n/translations/zh.ts index 2ec0d9f8..e38ff12f 100644 --- a/client/src/i18n/translations/zh.ts +++ b/client/src/i18n/translations/zh.ts @@ -1011,6 +1011,7 @@ const zh: Record = { '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 = { 'day.noPlacesForHotel': '请先在旅行中添加地点', 'day.allDays': '全部', 'day.checkIn': '入住', + 'day.checkInUntil': '截止', 'day.checkOut': '退房', 'day.confirmation': '确认号', 'day.editAccommodation': '编辑住宿', diff --git a/client/src/i18n/translations/zhTw.ts b/client/src/i18n/translations/zhTw.ts index 708c1156..a6709e15 100644 --- a/client/src/i18n/translations/zhTw.ts +++ b/client/src/i18n/translations/zhTw.ts @@ -1067,6 +1067,7 @@ const zhTw: Record = { '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 = { 'day.noPlacesForHotel': '請先在旅行中新增地點', 'day.allDays': '全部', 'day.checkIn': '入住', + 'day.checkInUntil': '截止', 'day.checkOut': '退房', 'day.confirmation': '確認號', 'day.editAccommodation': '編輯住宿', diff --git a/client/src/types.ts b/client/src/types.ts index 2cf23741..c0ef08db 100644 --- a/client/src/types.ts +++ b/client/src/types.ts @@ -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 diff --git a/server/src/db/migrations.ts b/server/src/db/migrations.ts index a63c5756..3b02bfe1 100644 --- a/server/src/db/migrations.ts +++ b/server/src/db/migrations.ts @@ -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) { diff --git a/server/src/db/schema.ts b/server/src/db/schema.ts index 9df9d013..40b5beba 100644 --- a/server/src/db/schema.ts +++ b/server/src/db/schema.ts @@ -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, diff --git a/server/src/routes/days.ts b/server/src/routes/days.ts index 60ff8b95..ce967355 100644 --- a/server/src/routes/days.ts +++ b/server/src/routes/days.ts @@ -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); }); diff --git a/server/src/services/dayService.ts b/server/src/services/dayService.ts index 705b364b..99e846d4 100644 --- a/server/src/services/dayService.ts +++ b/server/src/services/dayService.ts @@ -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 = {}; 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); diff --git a/server/src/services/reservationService.ts b/server/src/services/reservationService.ts index 22791f17..17628270 100644 --- a/server/src/services/reservationService.ts +++ b/server/src/services/reservationService.ts @@ -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) {