mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-20 13:51:45 +00:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6491e1f986 | |||
| 03757ed0af | |||
| a676dbe881 | |||
| 411d8620ba | |||
| f45f56318a | |||
| 3ae0f3f819 | |||
| 306626ee1c | |||
| 7e0fe3b1b9 | |||
| fdbc015dbf | |||
| 7d8e3912b4 | |||
| 9ebca725ae | |||
| 9718187490 | |||
| aa0620e01f |
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "trek-client",
|
"name": "trek-client",
|
||||||
"version": "2.8.4",
|
"version": "2.9.4",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "trek-client",
|
"name": "trek-client",
|
||||||
"version": "2.8.4",
|
"version": "2.9.4",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-pdf/renderer": "^4.3.2",
|
"@react-pdf/renderer": "^4.3.2",
|
||||||
"axios": "^1.6.7",
|
"axios": "^1.6.7",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "trek-client",
|
"name": "trek-client",
|
||||||
"version": "2.8.4",
|
"version": "2.9.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ export const reservationsApi = {
|
|||||||
create: (tripId: number | string, data: Record<string, unknown>) => apiClient.post(`/trips/${tripId}/reservations`, data).then(r => r.data),
|
create: (tripId: number | string, data: Record<string, unknown>) => apiClient.post(`/trips/${tripId}/reservations`, data).then(r => r.data),
|
||||||
update: (tripId: number | string, id: number, data: Record<string, unknown>) => apiClient.put(`/trips/${tripId}/reservations/${id}`, data).then(r => r.data),
|
update: (tripId: number | string, id: number, data: Record<string, unknown>) => apiClient.put(`/trips/${tripId}/reservations/${id}`, data).then(r => r.data),
|
||||||
delete: (tripId: number | string, id: number) => apiClient.delete(`/trips/${tripId}/reservations/${id}`).then(r => r.data),
|
delete: (tripId: number | string, id: number) => apiClient.delete(`/trips/${tripId}/reservations/${id}`).then(r => r.data),
|
||||||
updatePositions: (tripId: number | string, positions: { id: number; day_plan_position: number }[]) => apiClient.put(`/trips/${tripId}/reservations/positions`, { positions }).then(r => r.data),
|
updatePositions: (tripId: number | string, positions: { id: number; day_plan_position: number }[], dayId?: number) => apiClient.put(`/trips/${tripId}/reservations/positions`, { positions, day_id: dayId }).then(r => r.data),
|
||||||
}
|
}
|
||||||
|
|
||||||
export const weatherApi = {
|
export const weatherApi = {
|
||||||
|
|||||||
@@ -956,6 +956,9 @@ export default function MemoriesPanel({ tripId, startDate, endDate }: MemoriesPa
|
|||||||
setLightboxUserId(photo.user_id)
|
setLightboxUserId(photo.user_id)
|
||||||
setLightboxInfo(null)
|
setLightboxInfo(null)
|
||||||
fetchImageAsBlob('/api' + buildProviderAssetUrl(photo, 'original')).then(setLightboxOriginalSrc)
|
fetchImageAsBlob('/api' + buildProviderAssetUrl(photo, 'original')).then(setLightboxOriginalSrc)
|
||||||
|
setLightboxInfoLoading(true)
|
||||||
|
apiClient.get(buildProviderAssetUrl(photo, 'info'))
|
||||||
|
.then(r => setLightboxInfo(r.data)).catch(() => {}).finally(() => setLightboxInfoLoading(false))
|
||||||
}
|
}
|
||||||
|
|
||||||
const exifContent = lightboxInfo ? (
|
const exifContent = lightboxInfo ? (
|
||||||
|
|||||||
@@ -366,9 +366,12 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({
|
|||||||
const timed = timedTransports[ti]
|
const timed = timedTransports[ti]
|
||||||
const minutes = timed.minutes
|
const minutes = timed.minutes
|
||||||
|
|
||||||
// Use persisted position if available
|
// Use per-day position if available, fallback to global position
|
||||||
if (timed.data.day_plan_position != null) {
|
const dayObj = days.find(d => d.id === dayId)
|
||||||
result.push({ type: timed.type, sortKey: timed.data.day_plan_position, data: timed.data })
|
const perDayPos = timed.data.day_positions?.[dayId] ?? timed.data.day_positions?.[String(dayId)]
|
||||||
|
const effectivePos = perDayPos ?? timed.data.day_plan_position
|
||||||
|
if (effectivePos != null) {
|
||||||
|
result.push({ type: timed.type, sortKey: effectivePos, data: timed.data })
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -500,10 +503,15 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({
|
|||||||
if (transportUpdates.length) {
|
if (transportUpdates.length) {
|
||||||
for (const tu of transportUpdates) {
|
for (const tu of transportUpdates) {
|
||||||
const res = reservations.find(r => r.id === tu.id)
|
const res = reservations.find(r => r.id === tu.id)
|
||||||
if (res) res.day_plan_position = tu.day_plan_position
|
if (res) {
|
||||||
|
res.day_plan_position = tu.day_plan_position
|
||||||
|
// Update per-day position for multi-day reservations
|
||||||
|
if (!res.day_positions) res.day_positions = {}
|
||||||
|
res.day_positions[dayId] = tu.day_plan_position
|
||||||
|
}
|
||||||
}
|
}
|
||||||
setTransportPosVersion(v => v + 1)
|
setTransportPosVersion(v => v + 1)
|
||||||
await reservationsApi.updatePositions(tripId, transportUpdates)
|
await reservationsApi.updatePositions(tripId, transportUpdates, dayId)
|
||||||
}
|
}
|
||||||
if (prevAssignmentIds.length) {
|
if (prevAssignmentIds.length) {
|
||||||
const capturedDayId = dayId
|
const capturedDayId = dayId
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useState, useEffect, useRef, useMemo } from 'react'
|
|||||||
import { useParams } from 'react-router-dom'
|
import { useParams } from 'react-router-dom'
|
||||||
import apiClient from '../../api/client'
|
import apiClient from '../../api/client'
|
||||||
import { useTripStore } from '../../store/tripStore'
|
import { useTripStore } from '../../store/tripStore'
|
||||||
|
import { useAddonStore } from '../../store/addonStore'
|
||||||
import Modal from '../shared/Modal'
|
import Modal from '../shared/Modal'
|
||||||
import CustomSelect from '../shared/CustomSelect'
|
import CustomSelect from '../shared/CustomSelect'
|
||||||
import { Plane, Hotel, Utensils, Train, Car, Ship, Ticket, FileText, Users, Paperclip, X, ExternalLink, Link2 } from 'lucide-react'
|
import { Plane, Hotel, Utensils, Train, Car, Ship, Ticket, FileText, Users, Paperclip, X, ExternalLink, Link2 } from 'lucide-react'
|
||||||
@@ -71,6 +72,7 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p
|
|||||||
const { t, locale } = useTranslation()
|
const { t, locale } = useTranslation()
|
||||||
const fileInputRef = useRef(null)
|
const fileInputRef = useRef(null)
|
||||||
|
|
||||||
|
const isBudgetEnabled = useAddonStore(s => s.isEnabled('budget'))
|
||||||
const budgetItems = useTripStore(s => s.budgetItems)
|
const budgetItems = useTripStore(s => s.budgetItems)
|
||||||
const budgetCategories = useMemo(() => {
|
const budgetCategories = useMemo(() => {
|
||||||
const cats = new Set<string>()
|
const cats = new Set<string>()
|
||||||
@@ -139,7 +141,7 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p
|
|||||||
hotel_start_day: (() => { const acc = accommodations.find(a => a.id == reservation.accommodation_id); return acc?.start_day_id || '' })(),
|
hotel_start_day: (() => { const acc = accommodations.find(a => a.id == reservation.accommodation_id); return acc?.start_day_id || '' })(),
|
||||||
hotel_end_day: (() => { const acc = accommodations.find(a => a.id == reservation.accommodation_id); return acc?.end_day_id || '' })(),
|
hotel_end_day: (() => { const acc = accommodations.find(a => a.id == reservation.accommodation_id); return acc?.end_day_id || '' })(),
|
||||||
price: meta.price || '',
|
price: meta.price || '',
|
||||||
budget_category: meta.budget_category || '',
|
budget_category: (meta.budget_category && budgetItems.some(i => i.category === meta.budget_category)) ? meta.budget_category : '',
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
setForm({
|
setForm({
|
||||||
@@ -196,8 +198,10 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p
|
|||||||
if (form.end_date) {
|
if (form.end_date) {
|
||||||
combinedEndTime = form.reservation_end_time ? `${form.end_date}T${form.reservation_end_time}` : form.end_date
|
combinedEndTime = form.reservation_end_time ? `${form.end_date}T${form.reservation_end_time}` : form.end_date
|
||||||
}
|
}
|
||||||
if (form.price) metadata.price = form.price
|
if (isBudgetEnabled) {
|
||||||
if (form.budget_category) metadata.budget_category = form.budget_category
|
if (form.price) metadata.price = form.price
|
||||||
|
if (form.budget_category) metadata.budget_category = form.budget_category
|
||||||
|
}
|
||||||
const saveData: Record<string, any> = {
|
const saveData: Record<string, any> = {
|
||||||
title: form.title, type: form.type, status: form.status,
|
title: form.title, type: form.type, status: form.status,
|
||||||
reservation_time: form.reservation_time, reservation_end_time: combinedEndTime,
|
reservation_time: form.reservation_time, reservation_end_time: combinedEndTime,
|
||||||
@@ -208,9 +212,11 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p
|
|||||||
metadata: Object.keys(metadata).length > 0 ? metadata : null,
|
metadata: Object.keys(metadata).length > 0 ? metadata : null,
|
||||||
}
|
}
|
||||||
// Auto-create/update budget entry if price is set, or signal removal if cleared
|
// Auto-create/update budget entry if price is set, or signal removal if cleared
|
||||||
saveData.create_budget_entry = form.price && parseFloat(form.price) > 0
|
if (isBudgetEnabled) {
|
||||||
? { total_price: parseFloat(form.price), category: form.budget_category || t(`reservations.type.${form.type}`) || 'Other' }
|
saveData.create_budget_entry = form.price && parseFloat(form.price) > 0
|
||||||
: { total_price: 0 }
|
? { total_price: parseFloat(form.price), category: form.budget_category || t(`reservations.type.${form.type}`) || 'Other' }
|
||||||
|
: { total_price: 0 }
|
||||||
|
}
|
||||||
// If hotel with place + days, pass hotel data for auto-creation or update
|
// If hotel with place + days, pass hotel data for auto-creation or update
|
||||||
if (form.type === 'hotel' && form.hotel_place_id && form.hotel_start_day && form.hotel_end_day) {
|
if (form.type === 'hotel' && form.hotel_place_id && form.hotel_start_day && form.hotel_end_day) {
|
||||||
saveData.create_accommodation = {
|
saveData.create_accommodation = {
|
||||||
@@ -643,33 +649,37 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Price + Budget Category */}
|
{/* Price + Budget Category — only shown when budget addon is enabled */}
|
||||||
<div style={{ display: 'flex', gap: 8 }}>
|
{isBudgetEnabled && (
|
||||||
<div style={{ flex: 1, minWidth: 0 }}>
|
<>
|
||||||
<label style={labelStyle}>{t('reservations.price')}</label>
|
<div style={{ display: 'flex', gap: 8 }}>
|
||||||
<input type="text" inputMode="decimal" value={form.price}
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
onChange={e => { const v = e.target.value; if (v === '' || /^\d*\.?\d{0,2}$/.test(v)) set('price', v) }}
|
<label style={labelStyle}>{t('reservations.price')}</label>
|
||||||
placeholder="0.00"
|
<input type="text" inputMode="decimal" value={form.price}
|
||||||
style={inputStyle} />
|
onChange={e => { const v = e.target.value; if (v === '' || /^\d*\.?\d{0,2}$/.test(v)) set('price', v) }}
|
||||||
</div>
|
placeholder="0.00"
|
||||||
<div style={{ flex: 1, minWidth: 0 }}>
|
style={inputStyle} />
|
||||||
<label style={labelStyle}>{t('reservations.budgetCategory')}</label>
|
</div>
|
||||||
<CustomSelect
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
value={form.budget_category}
|
<label style={labelStyle}>{t('reservations.budgetCategory')}</label>
|
||||||
onChange={v => set('budget_category', v)}
|
<CustomSelect
|
||||||
options={[
|
value={form.budget_category}
|
||||||
{ value: '', label: t('reservations.budgetCategoryAuto') },
|
onChange={v => set('budget_category', v)}
|
||||||
...budgetCategories.map(c => ({ value: c, label: c })),
|
options={[
|
||||||
]}
|
{ value: '', label: t('reservations.budgetCategoryAuto') },
|
||||||
placeholder={t('reservations.budgetCategoryAuto')}
|
...budgetCategories.map(c => ({ value: c, label: c })),
|
||||||
size="sm"
|
]}
|
||||||
/>
|
placeholder={t('reservations.budgetCategoryAuto')}
|
||||||
</div>
|
size="sm"
|
||||||
</div>
|
/>
|
||||||
{form.price && parseFloat(form.price) > 0 && (
|
</div>
|
||||||
<div style={{ fontSize: 11, color: 'var(--text-faint)', marginTop: -4 }}>
|
</div>
|
||||||
{t('reservations.budgetHint')}
|
{form.price && parseFloat(form.price) > 0 && (
|
||||||
</div>
|
<div style={{ fontSize: 11, color: 'var(--text-faint)', marginTop: -4 }}>
|
||||||
|
{t('reservations.budgetHint')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
|
|||||||
@@ -137,6 +137,14 @@ export default function TripPlannerPage(): React.ReactElement | null {
|
|||||||
return saved || 'plan'
|
return saved || 'plan'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const validTabIds = TRIP_TABS.map(t => t.id)
|
||||||
|
if (!validTabIds.includes(activeTab)) {
|
||||||
|
setActiveTab('plan')
|
||||||
|
sessionStorage.setItem(`trip-tab-${tripId}`, 'plan')
|
||||||
|
}
|
||||||
|
}, [enabledAddons])
|
||||||
|
|
||||||
const handleTabChange = (tabId: string): void => {
|
const handleTabChange = (tabId: string): void => {
|
||||||
setActiveTab(tabId)
|
setActiveTab(tabId)
|
||||||
sessionStorage.setItem(`trip-tab-${tripId}`, tabId)
|
sessionStorage.setItem(`trip-tab-${tripId}`, tabId)
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "trek-server",
|
"name": "trek-server",
|
||||||
"version": "2.8.4",
|
"version": "2.9.4",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "trek-server",
|
"name": "trek-server",
|
||||||
"version": "2.8.4",
|
"version": "2.9.4",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.28.0",
|
"@modelcontextprotocol/sdk": "^1.28.0",
|
||||||
"archiver": "^6.0.1",
|
"archiver": "^6.0.1",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "trek-server",
|
"name": "trek-server",
|
||||||
"version": "2.8.4",
|
"version": "2.9.4",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node --import tsx src/index.ts",
|
"start": "node --import tsx src/index.ts",
|
||||||
|
|||||||
+1
-1
@@ -84,7 +84,7 @@ export function createApp(): express.Application {
|
|||||||
"https://unpkg.com", "https://open-meteo.com", "https://api.open-meteo.com",
|
"https://unpkg.com", "https://open-meteo.com", "https://api.open-meteo.com",
|
||||||
"https://geocoding-api.open-meteo.com", "https://api.exchangerate-api.com",
|
"https://geocoding-api.open-meteo.com", "https://api.exchangerate-api.com",
|
||||||
"https://raw.githubusercontent.com/nvkelso/natural-earth-vector/master/geojson/ne_50m_admin_0_countries.geojson",
|
"https://raw.githubusercontent.com/nvkelso/natural-earth-vector/master/geojson/ne_50m_admin_0_countries.geojson",
|
||||||
"https://router.project-osrm.org/route/v1"
|
"https://router.project-osrm.org/route/v1/"
|
||||||
],
|
],
|
||||||
fontSrc: ["'self'", "https://fonts.gstatic.com", "data:"],
|
fontSrc: ["'self'", "https://fonts.gstatic.com", "data:"],
|
||||||
objectSrc: ["'none'"],
|
objectSrc: ["'none'"],
|
||||||
|
|||||||
@@ -843,6 +843,27 @@ function runMigrations(db: Database.Database): void {
|
|||||||
const ins = db.prepare('INSERT OR IGNORE INTO packing_bag_members (bag_id, user_id) VALUES (?, ?)');
|
const ins = db.prepare('INSERT OR IGNORE INTO packing_bag_members (bag_id, user_id) VALUES (?, ?)');
|
||||||
for (const b of bagsWithUser) ins.run(b.id, b.user_id);
|
for (const b of bagsWithUser) ins.run(b.id, b.user_id);
|
||||||
},
|
},
|
||||||
|
// Migration: Per-day positions for multi-day reservations
|
||||||
|
() => {
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS reservation_day_positions (
|
||||||
|
reservation_id INTEGER NOT NULL REFERENCES reservations(id) ON DELETE CASCADE,
|
||||||
|
day_id INTEGER NOT NULL REFERENCES days(id) ON DELETE CASCADE,
|
||||||
|
position REAL NOT NULL,
|
||||||
|
PRIMARY KEY (reservation_id, day_id)
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
// Migrate existing global positions to per-day entries
|
||||||
|
const reservations = db.prepare('SELECT id, trip_id, reservation_time, reservation_end_time, day_plan_position FROM reservations WHERE day_plan_position IS NOT NULL').all() as any[];
|
||||||
|
const ins = db.prepare('INSERT OR IGNORE INTO reservation_day_positions (reservation_id, day_id, position) VALUES (?, ?, ?)');
|
||||||
|
for (const r of reservations) {
|
||||||
|
const startDate = r.reservation_time?.split('T')[0];
|
||||||
|
const endDate = r.reservation_end_time?.split('T')[0] || startDate;
|
||||||
|
if (!startDate) continue;
|
||||||
|
const matchingDays = db.prepare('SELECT id FROM days WHERE trip_id = ? AND date >= ? AND date <= ?').all(r.trip_id, startDate, endDate) as { id: number }[];
|
||||||
|
for (const d of matchingDays) ins.run(r.id, d.id, r.day_plan_position);
|
||||||
|
}
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (currentVersion < migrations.length) {
|
if (currentVersion < migrations.length) {
|
||||||
|
|||||||
@@ -91,10 +91,11 @@ router.put('/positions', authenticate, (req: Request, res: Response) => {
|
|||||||
|
|
||||||
if (!Array.isArray(positions)) return res.status(400).json({ error: 'positions must be an array' });
|
if (!Array.isArray(positions)) return res.status(400).json({ error: 'positions must be an array' });
|
||||||
|
|
||||||
updatePositions(tripId, positions);
|
const { day_id } = req.body;
|
||||||
|
updatePositions(tripId, positions, day_id);
|
||||||
|
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
broadcast(tripId, 'reservation:positions', { positions }, req.headers['x-socket-id'] as string);
|
broadcast(tripId, 'reservation:positions', { positions, day_id }, req.headers['x-socket-id'] as string);
|
||||||
});
|
});
|
||||||
|
|
||||||
router.put('/:id', authenticate, (req: Request, res: Response) => {
|
router.put('/:id', authenticate, (req: Request, res: Response) => {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export function verifyTripAccess(tripId: string | number, userId: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function listReservations(tripId: string | number) {
|
export function listReservations(tripId: string | number) {
|
||||||
return db.prepare(`
|
const reservations = db.prepare(`
|
||||||
SELECT r.*, d.day_number, p.name as place_name, r.assignment_id,
|
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
|
ap.place_id as accommodation_place_id, acc_p.name as accommodation_name
|
||||||
FROM reservations r
|
FROM reservations r
|
||||||
@@ -16,7 +16,27 @@ export function listReservations(tripId: string | number) {
|
|||||||
LEFT JOIN places acc_p ON ap.place_id = acc_p.id
|
LEFT JOIN places acc_p ON ap.place_id = acc_p.id
|
||||||
WHERE r.trip_id = ?
|
WHERE r.trip_id = ?
|
||||||
ORDER BY r.reservation_time ASC, r.created_at ASC
|
ORDER BY r.reservation_time ASC, r.created_at ASC
|
||||||
`).all(tripId);
|
`).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) {
|
export function getReservationWithJoins(id: string | number) {
|
||||||
@@ -117,14 +137,35 @@ export function createReservation(tripId: string | number, data: CreateReservati
|
|||||||
return { reservation, accommodationCreated };
|
return { reservation, accommodationCreated };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updatePositions(tripId: string | number, positions: { id: number; day_plan_position: number }[]) {
|
export function updatePositions(tripId: string | number, positions: { id: number; day_plan_position: number }[], dayId?: number | string) {
|
||||||
const stmt = db.prepare('UPDATE reservations SET day_plan_position = ? WHERE id = ? AND trip_id = ?');
|
if (dayId) {
|
||||||
const updateMany = db.transaction((items: { id: number; day_plan_position: number }[]) => {
|
// Per-day positions for multi-day reservations
|
||||||
for (const item of items) {
|
const stmt = db.prepare('INSERT OR REPLACE INTO reservation_day_positions (reservation_id, day_id, position) VALUES (?, ?, ?)');
|
||||||
stmt.run(item.day_plan_position, item.id, tripId);
|
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);
|
}
|
||||||
|
});
|
||||||
|
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) {
|
export function getReservation(id: string | number, tripId: string | number) {
|
||||||
|
|||||||
Reference in New Issue
Block a user