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')}
+
+ + {r.url} +
+
+ )} + {/* 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(),