Reordering places on one day of a multi-day reservation no longer affects the order on other days. Transport positions are now stored per-day in a new reservation_day_positions table instead of a single global day_plan_position on the reservation.
284 lines
12 KiB
TypeScript
284 lines
12 KiB
TypeScript
import { db, canAccessTrip } from '../db/database';
|
|
import { Reservation } from '../types';
|
|
|
|
export function verifyTripAccess(tripId: string | number, userId: number) {
|
|
return canAccessTrip(tripId, userId);
|
|
}
|
|
|
|
export function listReservations(tripId: string | number) {
|
|
const reservations = db.prepare(`
|
|
SELECT r.*, d.day_number, p.name as place_name, r.assignment_id,
|
|
ap.place_id as accommodation_place_id, acc_p.name as accommodation_name
|
|
FROM reservations r
|
|
LEFT JOIN days d ON r.day_id = d.id
|
|
LEFT JOIN places p ON r.place_id = p.id
|
|
LEFT JOIN day_accommodations ap ON r.accommodation_id = ap.id
|
|
LEFT JOIN places acc_p ON ap.place_id = acc_p.id
|
|
WHERE r.trip_id = ?
|
|
ORDER BY r.reservation_time ASC, r.created_at ASC
|
|
`).all(tripId) as any[];
|
|
|
|
// Attach per-day positions for multi-day reservations
|
|
const dayPositions = db.prepare(`
|
|
SELECT rdp.reservation_id, rdp.day_id, rdp.position
|
|
FROM reservation_day_positions rdp
|
|
JOIN reservations r ON rdp.reservation_id = r.id
|
|
WHERE r.trip_id = ?
|
|
`).all(tripId) as { reservation_id: number; day_id: number; position: number }[];
|
|
|
|
const posMap = new Map<number, Record<number, number>>();
|
|
for (const dp of dayPositions) {
|
|
if (!posMap.has(dp.reservation_id)) posMap.set(dp.reservation_id, {});
|
|
posMap.get(dp.reservation_id)![dp.day_id] = dp.position;
|
|
}
|
|
|
|
for (const r of reservations) {
|
|
r.day_positions = posMap.get(r.id) || null;
|
|
}
|
|
|
|
return reservations;
|
|
}
|
|
|
|
export function getReservationWithJoins(id: string | number) {
|
|
return db.prepare(`
|
|
SELECT r.*, d.day_number, p.name as place_name, r.assignment_id,
|
|
ap.place_id as accommodation_place_id, acc_p.name as accommodation_name
|
|
FROM reservations r
|
|
LEFT JOIN days d ON r.day_id = d.id
|
|
LEFT JOIN places p ON r.place_id = p.id
|
|
LEFT JOIN day_accommodations ap ON r.accommodation_id = ap.id
|
|
LEFT JOIN places acc_p ON ap.place_id = acc_p.id
|
|
WHERE r.id = ?
|
|
`).get(id);
|
|
}
|
|
|
|
interface CreateAccommodation {
|
|
place_id?: number;
|
|
start_day_id?: number;
|
|
end_day_id?: number;
|
|
check_in?: string;
|
|
check_out?: string;
|
|
confirmation?: string;
|
|
}
|
|
|
|
interface CreateReservationData {
|
|
title: string;
|
|
reservation_time?: string;
|
|
reservation_end_time?: string;
|
|
location?: string;
|
|
confirmation_number?: string;
|
|
notes?: string;
|
|
day_id?: number;
|
|
place_id?: number;
|
|
assignment_id?: number;
|
|
status?: string;
|
|
type?: string;
|
|
accommodation_id?: number;
|
|
metadata?: any;
|
|
create_accommodation?: CreateAccommodation;
|
|
}
|
|
|
|
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, place_id, assignment_id,
|
|
status, type, accommodation_id, metadata, create_accommodation
|
|
} = data;
|
|
|
|
let accommodationCreated = false;
|
|
|
|
// Auto-create accommodation for hotel reservations
|
|
let resolvedAccommodationId: number | null = accommodation_id || null;
|
|
if (type === 'hotel' && !resolvedAccommodationId && create_accommodation) {
|
|
const { place_id: accPlaceId, start_day_id, end_day_id, check_in, check_out, confirmation: accConf } = create_accommodation;
|
|
if (accPlaceId && start_day_id && end_day_id) {
|
|
const accResult = db.prepare(
|
|
'INSERT INTO day_accommodations (trip_id, place_id, start_day_id, end_day_id, check_in, check_out, confirmation) VALUES (?, ?, ?, ?, ?, ?, ?)'
|
|
).run(tripId, accPlaceId, start_day_id, end_day_id, check_in || null, check_out || null, accConf || confirmation_number || null);
|
|
resolvedAccommodationId = Number(accResult.lastInsertRowid);
|
|
accommodationCreated = true;
|
|
}
|
|
}
|
|
|
|
const result = db.prepare(`
|
|
INSERT INTO reservations (trip_id, day_id, place_id, assignment_id, title, reservation_time, reservation_end_time, location, confirmation_number, notes, status, type, accommodation_id, metadata)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
`).run(
|
|
tripId,
|
|
day_id || null,
|
|
place_id || null,
|
|
assignment_id || null,
|
|
title,
|
|
reservation_time || null,
|
|
reservation_end_time || null,
|
|
location || null,
|
|
confirmation_number || null,
|
|
notes || null,
|
|
status || 'pending',
|
|
type || 'other',
|
|
resolvedAccommodationId,
|
|
metadata ? JSON.stringify(metadata) : null
|
|
);
|
|
|
|
// 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 (confirmation_number) {
|
|
db.prepare('UPDATE day_accommodations SET confirmation = COALESCE(?, confirmation) WHERE id = ?')
|
|
.run(confirmation_number, accommodation_id);
|
|
}
|
|
}
|
|
|
|
const reservation = getReservationWithJoins(Number(result.lastInsertRowid));
|
|
return { reservation, accommodationCreated };
|
|
}
|
|
|
|
export function updatePositions(tripId: string | number, positions: { id: number; day_plan_position: number }[], dayId?: number | string) {
|
|
if (dayId) {
|
|
// Per-day positions for multi-day reservations
|
|
const stmt = db.prepare('INSERT OR REPLACE INTO reservation_day_positions (reservation_id, day_id, position) VALUES (?, ?, ?)');
|
|
const updateMany = db.transaction((items: { id: number; day_plan_position: number }[]) => {
|
|
for (const item of items) {
|
|
stmt.run(item.id, dayId, item.day_plan_position);
|
|
}
|
|
});
|
|
updateMany(positions);
|
|
} else {
|
|
// Legacy: update global position
|
|
const stmt = db.prepare('UPDATE reservations SET day_plan_position = ? WHERE id = ? AND trip_id = ?');
|
|
const updateMany = db.transaction((items: { id: number; day_plan_position: number }[]) => {
|
|
for (const item of items) {
|
|
stmt.run(item.day_plan_position, item.id, tripId);
|
|
}
|
|
});
|
|
updateMany(positions);
|
|
}
|
|
}
|
|
|
|
export function getDayPositions(tripId: string | number, dayId: number | string) {
|
|
return db.prepare(`
|
|
SELECT rdp.reservation_id, rdp.position
|
|
FROM reservation_day_positions rdp
|
|
JOIN reservations r ON rdp.reservation_id = r.id
|
|
WHERE r.trip_id = ? AND rdp.day_id = ?
|
|
`).all(tripId, dayId) as { reservation_id: number; position: number }[];
|
|
}
|
|
|
|
export function getReservation(id: string | number, tripId: string | number) {
|
|
return db.prepare('SELECT * FROM reservations WHERE id = ? AND trip_id = ?').get(id, tripId) as Reservation | undefined;
|
|
}
|
|
|
|
interface UpdateReservationData {
|
|
title?: string;
|
|
reservation_time?: string;
|
|
reservation_end_time?: string;
|
|
location?: string;
|
|
confirmation_number?: string;
|
|
notes?: string;
|
|
day_id?: number;
|
|
place_id?: number;
|
|
assignment_id?: number;
|
|
status?: string;
|
|
type?: string;
|
|
accommodation_id?: number;
|
|
metadata?: any;
|
|
create_accommodation?: CreateAccommodation;
|
|
}
|
|
|
|
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, place_id, assignment_id,
|
|
status, type, accommodation_id, metadata, create_accommodation
|
|
} = data;
|
|
|
|
let accommodationChanged = false;
|
|
|
|
// Update or create accommodation for hotel reservations
|
|
let resolvedAccId: number | null = accommodation_id !== undefined ? (accommodation_id || null) : (current.accommodation_id ?? null);
|
|
if (type === 'hotel' && create_accommodation) {
|
|
const { place_id: accPlaceId, start_day_id, end_day_id, check_in, check_out, confirmation: accConf } = create_accommodation;
|
|
if (accPlaceId && start_day_id && end_day_id) {
|
|
if (resolvedAccId) {
|
|
db.prepare('UPDATE day_accommodations SET place_id = ?, start_day_id = ?, end_day_id = ?, check_in = ?, check_out = ?, confirmation = ? WHERE id = ?')
|
|
.run(accPlaceId, start_day_id, end_day_id, check_in || null, check_out || null, accConf || confirmation_number || null, resolvedAccId);
|
|
} else {
|
|
const accResult = db.prepare(
|
|
'INSERT INTO day_accommodations (trip_id, place_id, start_day_id, end_day_id, check_in, check_out, confirmation) VALUES (?, ?, ?, ?, ?, ?, ?)'
|
|
).run(tripId, accPlaceId, start_day_id, end_day_id, check_in || null, check_out || null, accConf || confirmation_number || null);
|
|
resolvedAccId = Number(accResult.lastInsertRowid);
|
|
}
|
|
accommodationChanged = true;
|
|
}
|
|
}
|
|
|
|
db.prepare(`
|
|
UPDATE reservations SET
|
|
title = COALESCE(?, title),
|
|
reservation_time = ?,
|
|
reservation_end_time = ?,
|
|
location = ?,
|
|
confirmation_number = ?,
|
|
notes = ?,
|
|
day_id = ?,
|
|
place_id = ?,
|
|
assignment_id = ?,
|
|
status = COALESCE(?, status),
|
|
type = COALESCE(?, type),
|
|
accommodation_id = ?,
|
|
metadata = ?
|
|
WHERE id = ?
|
|
`).run(
|
|
title || null,
|
|
reservation_time !== undefined ? (reservation_time || null) : current.reservation_time,
|
|
reservation_end_time !== undefined ? (reservation_end_time || null) : current.reservation_end_time,
|
|
location !== undefined ? (location || null) : current.location,
|
|
confirmation_number !== undefined ? (confirmation_number || null) : current.confirmation_number,
|
|
notes !== undefined ? (notes || null) : current.notes,
|
|
day_id !== undefined ? (day_id || null) : current.day_id,
|
|
place_id !== undefined ? (place_id || null) : current.place_id,
|
|
assignment_id !== undefined ? (assignment_id || null) : current.assignment_id,
|
|
status || null,
|
|
type || null,
|
|
resolvedAccId,
|
|
metadata !== undefined ? (metadata ? JSON.stringify(metadata) : null) : current.metadata,
|
|
id
|
|
);
|
|
|
|
// Sync check-in/out to accommodation if linked
|
|
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);
|
|
}
|
|
const resolvedConf = confirmation_number !== undefined ? confirmation_number : current.confirmation_number;
|
|
if (resolvedConf) {
|
|
db.prepare('UPDATE day_accommodations SET confirmation = COALESCE(?, confirmation) WHERE id = ?')
|
|
.run(resolvedConf, resolvedAccId);
|
|
}
|
|
}
|
|
|
|
const reservation = getReservationWithJoins(id);
|
|
return { reservation, accommodationChanged };
|
|
}
|
|
|
|
export function deleteReservation(id: string | number, tripId: string | number): { deleted: { id: number; title: string; type: string; accommodation_id: number | null } | undefined; accommodationDeleted: boolean } {
|
|
const reservation = db.prepare('SELECT id, title, type, accommodation_id FROM reservations WHERE id = ? AND trip_id = ?').get(id, tripId) as { id: number; title: string; type: string; accommodation_id: number | null } | undefined;
|
|
if (!reservation) return { deleted: undefined, accommodationDeleted: false };
|
|
|
|
let accommodationDeleted = false;
|
|
if (reservation.accommodation_id) {
|
|
db.prepare('DELETE FROM day_accommodations WHERE id = ?').run(reservation.accommodation_id);
|
|
accommodationDeleted = true;
|
|
}
|
|
|
|
db.prepare('DELETE FROM reservations WHERE id = ?').run(id);
|
|
return { deleted: reservation, accommodationDeleted };
|
|
}
|