diff --git a/client/src/components/Planner/ReservationModal.tsx b/client/src/components/Planner/ReservationModal.tsx
index d94b302f5..c1a15b9a4 100644
--- a/client/src/components/Planner/ReservationModal.tsx
+++ b/client/src/components/Planner/ReservationModal.tsx
@@ -86,7 +86,7 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p
const [form, setForm] = useState({
title: '', type: 'other', status: 'pending',
reservation_time: '', reservation_end_time: '', end_date: '', location: '', confirmation_number: '',
- notes: '', assignment_id: '' as string | number, accommodation_id: '' as string | number,
+ notes: '', url: '', assignment_id: '' as string | number, accommodation_id: '' as string | number,
meta_check_in_time: '', meta_check_in_end_time: '', meta_check_out_time: '',
hotel_place_id: '' as string | number, hotel_start_day: '' as string | number, hotel_end_day: '' as string | number,
hotel_address: '',
@@ -136,6 +136,7 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p
location: reservation.location || '',
confirmation_number: reservation.confirmation_number || '',
notes: reservation.notes || '',
+ url: reservation.url || '',
assignment_id: reservation.assignment_id || '',
accommodation_id: reservation.accommodation_id || '',
meta_check_in_time: meta.check_in_time || '',
@@ -164,6 +165,7 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p
location: prefill.location || '',
confirmation_number: prefill.confirmation_number || '',
notes: prefill.notes || '',
+ url: (prefill as { url?: string }).url || '',
assignment_id: defaultAssignmentId ?? '',
accommodation_id: '',
meta_check_in_time: meta.check_in_time || '',
@@ -180,7 +182,7 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p
setForm({
title: '', type: 'other', status: 'pending',
reservation_time: '', reservation_end_time: '', end_date: '', location: '', confirmation_number: '',
- notes: '', assignment_id: defaultAssignmentId ?? '', accommodation_id: '',
+ notes: '', url: '', assignment_id: defaultAssignmentId ?? '', accommodation_id: '',
meta_check_in_time: '', meta_check_in_end_time: '', meta_check_out_time: '',
hotel_place_id: '', hotel_start_day: '', hotel_end_day: '', hotel_address: '',
})
@@ -237,6 +239,7 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p
reservation_end_time: form.type === 'hotel' ? null : (combinedEndTime || null),
location: form.location, confirmation_number: form.confirmation_number,
notes: form.notes,
+ url: form.url,
assignment_id: (form.type === 'hotel' && !form.accommodation_id) ? null : (form.assignment_id || null),
accommodation_id: form.type === 'hotel' ? (form.accommodation_id || null) : null,
metadata: Object.keys(metadata).length > 0 ? metadata : null,
@@ -591,6 +594,16 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p
>
)}
+ {/* Link */}
+
+
+
+
+ set('url', e.target.value)}
+ placeholder={t('reservations.urlPlaceholder')} className={inputClass} style={{ paddingLeft: 34 }} />
+
+
+
{/* Notes */}
diff --git a/client/src/components/Planner/ReservationsPanel.tsx b/client/src/components/Planner/ReservationsPanel.tsx
index 2ad4afea3..c22d7d8d6 100644
--- a/client/src/components/Planner/ReservationsPanel.tsx
+++ b/client/src/components/Planner/ReservationsPanel.tsx
@@ -361,6 +361,18 @@ function ReservationCard({ r, tripId, onEdit, onDelete, files = [], onNavigateTo
)}
+ {/* Link */}
+ {r.url && (
+
+
{t('reservations.urlLabel')}
+
+
+ )}
+
{/* Notes */}
{r.notes && (
diff --git a/server/src/db/migrations.ts b/server/src/db/migrations.ts
index f99e56862..bcd323bb2 100644
--- a/server/src/db/migrations.ts
+++ b/server/src/db/migrations.ts
@@ -3123,6 +3123,15 @@ function runMigrations(db: Database.Database): void {
}
}
},
+ // Dedicated booking URL (#935) — users previously stuffed links into notes.
+ // Additive nullable TEXT; existing rows default to NULL.
+ () => {
+ try {
+ db.exec('ALTER TABLE reservations ADD COLUMN url TEXT');
+ } catch (err: any) {
+ if (!err.message?.includes('duplicate column name')) throw err;
+ }
+ },
];
if (currentVersion < migrations.length) {
diff --git a/server/src/services/reservationService.ts b/server/src/services/reservationService.ts
index 2e2b05fbd..7b1952437 100644
--- a/server/src/services/reservationService.ts
+++ b/server/src/services/reservationService.ts
@@ -232,6 +232,7 @@ interface CreateReservationData {
location?: string;
confirmation_number?: string;
notes?: string;
+ url?: string;
day_id?: number;
end_day_id?: number;
place_id?: number;
@@ -248,7 +249,7 @@ interface CreateReservationData {
export function createReservation(tripId: string | number, data: CreateReservationData): { reservation: any; accommodationCreated: boolean } {
const {
title, reservation_time, reservation_end_time, location,
- confirmation_number, notes, day_id, end_day_id, place_id, assignment_id,
+ confirmation_number, notes, url, day_id, end_day_id, place_id, assignment_id,
status, type, accommodation_id, metadata, create_accommodation,
endpoints, needs_review
} = data;
@@ -282,8 +283,8 @@ export function createReservation(tripId: string | number, data: CreateReservati
}
const result = db.prepare(`
- INSERT INTO reservations (trip_id, day_id, end_day_id, place_id, assignment_id, title, reservation_time, reservation_end_time, location, confirmation_number, notes, status, type, accommodation_id, metadata, needs_review)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ INSERT INTO reservations (trip_id, day_id, end_day_id, place_id, assignment_id, title, reservation_time, reservation_end_time, location, confirmation_number, notes, url, status, type, accommodation_id, metadata, needs_review)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
tripId,
resolvedDayId,
@@ -296,6 +297,7 @@ export function createReservation(tripId: string | number, data: CreateReservati
location || null,
confirmation_number || null,
notes || null,
+ url || null,
status || 'pending',
resolvedType,
resolvedAccommodationId,
@@ -357,6 +359,7 @@ interface UpdateReservationData {
location?: string;
confirmation_number?: string;
notes?: string;
+ url?: string;
day_id?: number;
end_day_id?: number | null;
place_id?: number;
@@ -373,7 +376,7 @@ interface UpdateReservationData {
export function updateReservation(id: string | number, tripId: string | number, data: UpdateReservationData, current: Reservation): { reservation: any; accommodationChanged: boolean } {
const {
title, reservation_time, reservation_end_time, location,
- confirmation_number, notes, day_id, end_day_id, place_id, assignment_id,
+ confirmation_number, notes, url, day_id, end_day_id, place_id, assignment_id,
status, type, accommodation_id, metadata, create_accommodation,
endpoints, needs_review
} = data;
@@ -446,6 +449,7 @@ export function updateReservation(id: string | number, tripId: string | number,
location = ?,
confirmation_number = ?,
notes = ?,
+ url = ?,
day_id = ?,
end_day_id = ?,
place_id = ?,
@@ -463,6 +467,7 @@ export function updateReservation(id: string | number, tripId: string | number,
location !== undefined ? (location || null) : current.location,
confirmation_number !== undefined ? (confirmation_number || null) : current.confirmation_number,
notes !== undefined ? (notes || null) : current.notes,
+ url !== undefined ? (url || null) : (current as any).url,
nextDayId,
nextEndDayId,
place_id !== undefined ? (place_id || null) : current.place_id,
diff --git a/server/tests/integration/reservations.test.ts b/server/tests/integration/reservations.test.ts
index 36c08f3a5..8b70f9ba4 100644
--- a/server/tests/integration/reservations.test.ts
+++ b/server/tests/integration/reservations.test.ts
@@ -88,6 +88,25 @@ describe('Create reservation', () => {
expect(res.body.reservation.type).toBe('hotel');
});
+ it('RESV-001b — persists and updates the dedicated url field (#935)', async () => {
+ const { user } = createUser(testDb);
+ const trip = createTrip(testDb, user.id);
+
+ const created = await request(app)
+ .post(`/api/trips/${trip.id}/reservations`)
+ .set('Cookie', authCookie(user.id))
+ .send({ title: 'Hotel', type: 'hotel', url: 'https://hotel.example/booking' });
+ expect(created.status).toBe(201);
+ expect(created.body.reservation.url).toBe('https://hotel.example/booking');
+
+ const updated = await request(app)
+ .put(`/api/trips/${trip.id}/reservations/${created.body.reservation.id}`)
+ .set('Cookie', authCookie(user.id))
+ .send({ url: 'https://hotel.example/changed' });
+ expect(updated.status).toBe(200);
+ expect(updated.body.reservation.url).toBe('https://hotel.example/changed');
+ });
+
it('RESV-001 — POST without title returns 400', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
diff --git a/shared/src/i18n/de/reservations.ts b/shared/src/i18n/de/reservations.ts
index 26468f2a4..df4931dcd 100644
--- a/shared/src/i18n/de/reservations.ts
+++ b/shared/src/i18n/de/reservations.ts
@@ -23,6 +23,8 @@ const reservations: TranslationStrings = {
'reservations.timeAlt': 'Uhrzeit (alternativ, z.B. 19:30)',
'reservations.notes': 'Notizen',
'reservations.notesPlaceholder': 'Zusätzliche Notizen...',
+ 'reservations.urlLabel': 'Link',
+ 'reservations.urlPlaceholder': 'https://...',
'reservations.meta.airline': 'Fluggesellschaft',
'reservations.meta.flightNumber': 'Flugnr.',
'reservations.meta.from': 'Von',
diff --git a/shared/src/i18n/en/reservations.ts b/shared/src/i18n/en/reservations.ts
index 365f3d8e9..754e8e157 100644
--- a/shared/src/i18n/en/reservations.ts
+++ b/shared/src/i18n/en/reservations.ts
@@ -22,6 +22,8 @@ const reservations: TranslationStrings = {
'reservations.timeAlt': 'Time (alternative, e.g. 19:30)',
'reservations.notes': 'Notes',
'reservations.notesPlaceholder': 'Additional notes...',
+ 'reservations.urlLabel': 'Link',
+ 'reservations.urlPlaceholder': 'https://...',
'reservations.meta.airline': 'Airline',
'reservations.meta.flightNumber': 'Flight No.',
'reservations.meta.from': 'From',
diff --git a/shared/src/reservation/reservation.schema.ts b/shared/src/reservation/reservation.schema.ts
index 70543999d..c70f1d9ca 100644
--- a/shared/src/reservation/reservation.schema.ts
+++ b/shared/src/reservation/reservation.schema.ts
@@ -54,6 +54,7 @@ export const reservationSchema = z.object({
location: z.string().nullable().optional(),
confirmation_number: z.string().nullable().optional(),
notes: z.string().nullable().optional(),
+ url: z.string().nullable().optional(),
status: z.string(),
type: z.string(),
accommodation_id: z.union([z.number(), z.string()]).nullable().optional(),