Fix double delete confirm, inline place name editing, preserve assignments on trip extend
- Replace double browser confirm() with single custom ConfirmDialog for place deletion - Add inline name editing via double-click in PlaceInspector - Rewrite generateDays() to preserve existing days/assignments when extending trips - Use UTC date math to avoid timezone-related day count errors - Add missing collab.chat.emptyDesc translation (en/de)
This commit is contained in:
@@ -674,7 +674,7 @@ export default function DayPlanSidebar({
|
|||||||
place.website && { label: t('inspector.website'), icon: ExternalLink, onClick: () => window.open(place.website, '_blank') },
|
place.website && { label: t('inspector.website'), icon: ExternalLink, onClick: () => window.open(place.website, '_blank') },
|
||||||
(place.lat && place.lng) && { label: 'Google Maps', icon: Navigation, onClick: () => window.open(`https://www.google.com/maps/search/?api=1&query=${place.lat},${place.lng}`, '_blank') },
|
(place.lat && place.lng) && { label: 'Google Maps', icon: Navigation, onClick: () => window.open(`https://www.google.com/maps/search/?api=1&query=${place.lat},${place.lng}`, '_blank') },
|
||||||
{ divider: true },
|
{ divider: true },
|
||||||
onDeletePlace && { label: t('common.delete'), icon: Trash2, danger: true, onClick: () => { if (confirm(t('trip.confirm.deletePlace'))) onDeletePlace(place.id) } },
|
onDeletePlace && { label: t('common.delete'), icon: Trash2, danger: true, onClick: () => onDeletePlace(place.id) },
|
||||||
])}
|
])}
|
||||||
onMouseEnter={() => setHoveredId(assignment.id)}
|
onMouseEnter={() => setHoveredId(assignment.id)}
|
||||||
onMouseLeave={() => setHoveredId(null)}
|
onMouseLeave={() => setHoveredId(null)}
|
||||||
|
|||||||
@@ -100,16 +100,39 @@ function formatFileSize(bytes) {
|
|||||||
export default function PlaceInspector({
|
export default function PlaceInspector({
|
||||||
place, categories, days, selectedDayId, selectedAssignmentId, assignments, reservations = [],
|
place, categories, days, selectedDayId, selectedAssignmentId, assignments, reservations = [],
|
||||||
onClose, onEdit, onDelete, onAssignToDay, onRemoveAssignment,
|
onClose, onEdit, onDelete, onAssignToDay, onRemoveAssignment,
|
||||||
files, onFileUpload, tripMembers = [], onSetParticipants,
|
files, onFileUpload, tripMembers = [], onSetParticipants, onUpdatePlace,
|
||||||
}) {
|
}) {
|
||||||
const { t, locale, language } = useTranslation()
|
const { t, locale, language } = useTranslation()
|
||||||
const timeFormat = useSettingsStore(s => s.settings.time_format) || '24h'
|
const timeFormat = useSettingsStore(s => s.settings.time_format) || '24h'
|
||||||
const [hoursExpanded, setHoursExpanded] = useState(false)
|
const [hoursExpanded, setHoursExpanded] = useState(false)
|
||||||
const [filesExpanded, setFilesExpanded] = useState(false)
|
const [filesExpanded, setFilesExpanded] = useState(false)
|
||||||
const [isUploading, setIsUploading] = useState(false)
|
const [isUploading, setIsUploading] = useState(false)
|
||||||
|
const [editingName, setEditingName] = useState(false)
|
||||||
|
const [nameValue, setNameValue] = useState('')
|
||||||
|
const nameInputRef = useRef(null)
|
||||||
const fileInputRef = useRef(null)
|
const fileInputRef = useRef(null)
|
||||||
const googleDetails = useGoogleDetails(place?.google_place_id, language)
|
const googleDetails = useGoogleDetails(place?.google_place_id, language)
|
||||||
|
|
||||||
|
const startNameEdit = () => {
|
||||||
|
if (!onUpdatePlace) return
|
||||||
|
setNameValue(place.name || '')
|
||||||
|
setEditingName(true)
|
||||||
|
setTimeout(() => nameInputRef.current?.focus(), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
const commitNameEdit = () => {
|
||||||
|
if (!editingName) return
|
||||||
|
const trimmed = nameValue.trim()
|
||||||
|
setEditingName(false)
|
||||||
|
if (!trimmed || trimmed === place.name) return
|
||||||
|
onUpdatePlace(place.id, { name: trimmed })
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleNameKeyDown = (e) => {
|
||||||
|
if (e.key === 'Enter') { e.preventDefault(); commitNameEdit() }
|
||||||
|
if (e.key === 'Escape') setEditingName(false)
|
||||||
|
}
|
||||||
|
|
||||||
if (!place) return null
|
if (!place) return null
|
||||||
|
|
||||||
const category = categories?.find(c => c.id === place.category_id)
|
const category = categories?.find(c => c.id === place.category_id)
|
||||||
@@ -192,7 +215,21 @@ export default function PlaceInspector({
|
|||||||
</div>
|
</div>
|
||||||
<div style={{ flex: 1, minWidth: 0 }}>
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' }}>
|
||||||
<span style={{ fontWeight: 600, fontSize: 15, color: 'var(--text-primary)', lineHeight: '1.3' }}>{place.name}</span>
|
{editingName ? (
|
||||||
|
<input
|
||||||
|
ref={nameInputRef}
|
||||||
|
value={nameValue}
|
||||||
|
onChange={e => setNameValue(e.target.value)}
|
||||||
|
onBlur={commitNameEdit}
|
||||||
|
onKeyDown={handleNameKeyDown}
|
||||||
|
style={{ fontWeight: 600, fontSize: 15, color: 'var(--text-primary)', lineHeight: '1.3', background: 'var(--bg-secondary)', border: '1px solid var(--border-primary)', borderRadius: 6, padding: '1px 6px', fontFamily: 'inherit', outline: 'none', width: '100%' }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span
|
||||||
|
onDoubleClick={startNameEdit}
|
||||||
|
style={{ fontWeight: 600, fontSize: 15, color: 'var(--text-primary)', lineHeight: '1.3', cursor: onUpdatePlace ? 'text' : 'default' }}
|
||||||
|
>{place.name}</span>
|
||||||
|
)}
|
||||||
{category && (() => {
|
{category && (() => {
|
||||||
const CatIcon = getCategoryIcon(category.icon)
|
const CatIcon = getCategoryIcon(category.icon)
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ export default function PlacesSidebar({
|
|||||||
place.website && { label: t('inspector.website'), icon: ExternalLink, onClick: () => window.open(place.website, '_blank') },
|
place.website && { label: t('inspector.website'), icon: ExternalLink, onClick: () => window.open(place.website, '_blank') },
|
||||||
(place.lat && place.lng) && { label: 'Google Maps', icon: Navigation, onClick: () => window.open(`https://www.google.com/maps/search/?api=1&query=${place.lat},${place.lng}`, '_blank') },
|
(place.lat && place.lng) && { label: 'Google Maps', icon: Navigation, onClick: () => window.open(`https://www.google.com/maps/search/?api=1&query=${place.lat},${place.lng}`, '_blank') },
|
||||||
{ divider: true },
|
{ divider: true },
|
||||||
onDeletePlace && { label: t('common.delete'), icon: Trash2, danger: true, onClick: () => { if (confirm(t('trip.confirm.deletePlace'))) onDeletePlace(place.id) } },
|
onDeletePlace && { label: t('common.delete'), icon: Trash2, danger: true, onClick: () => onDeletePlace(place.id) },
|
||||||
])}
|
])}
|
||||||
style={{
|
style={{
|
||||||
display: 'flex', alignItems: 'center', gap: 10,
|
display: 'flex', alignItems: 'center', gap: 10,
|
||||||
|
|||||||
90
client/src/components/shared/ConfirmDialog.jsx
Normal file
90
client/src/components/shared/ConfirmDialog.jsx
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import React, { useEffect, useCallback } from 'react'
|
||||||
|
import { AlertTriangle } from 'lucide-react'
|
||||||
|
import { useTranslation } from '../../i18n'
|
||||||
|
|
||||||
|
export default function ConfirmDialog({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
onConfirm,
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
confirmLabel,
|
||||||
|
cancelLabel,
|
||||||
|
danger = true,
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const handleEsc = useCallback((e) => {
|
||||||
|
if (e.key === 'Escape') onClose()
|
||||||
|
}, [onClose])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
document.addEventListener('keydown', handleEsc)
|
||||||
|
}
|
||||||
|
return () => document.removeEventListener('keydown', handleEsc)
|
||||||
|
}, [isOpen, handleEsc])
|
||||||
|
|
||||||
|
if (!isOpen) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-[60] flex items-center justify-center px-4"
|
||||||
|
style={{ backgroundColor: 'rgba(15, 23, 42, 0.5)' }}
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="rounded-2xl shadow-2xl w-full max-w-sm p-6"
|
||||||
|
style={{
|
||||||
|
animation: 'modalIn 0.2s ease-out forwards',
|
||||||
|
background: 'var(--bg-card)',
|
||||||
|
}}
|
||||||
|
onClick={e => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
{danger && (
|
||||||
|
<div className="flex-shrink-0 w-10 h-10 rounded-full bg-red-100 flex items-center justify-center">
|
||||||
|
<AlertTriangle className="w-5 h-5 text-red-600" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className="text-base font-semibold" style={{ color: 'var(--text-primary)' }}>
|
||||||
|
{title || t('common.confirm')}
|
||||||
|
</h3>
|
||||||
|
<p className="mt-1 text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||||||
|
{message}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-3 mt-6">
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-4 py-2 text-sm font-medium rounded-lg transition-colors"
|
||||||
|
style={{
|
||||||
|
color: 'var(--text-secondary)',
|
||||||
|
border: '1px solid var(--border-secondary)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{cancelLabel || t('common.cancel')}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => { onConfirm(); onClose() }}
|
||||||
|
className={`px-4 py-2 text-sm font-medium rounded-lg transition-colors text-white ${
|
||||||
|
danger ? 'bg-red-600 hover:bg-red-700' : 'bg-blue-600 hover:bg-blue-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{confirmLabel || t('common.delete')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>{`
|
||||||
|
@keyframes modalIn {
|
||||||
|
from { opacity: 0; transform: scale(0.95) translateY(-10px); }
|
||||||
|
to { opacity: 1; transform: scale(1) translateY(0); }
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -956,6 +956,7 @@ const de = {
|
|||||||
'collab.chat.placeholder': 'Nachricht eingeben...',
|
'collab.chat.placeholder': 'Nachricht eingeben...',
|
||||||
'collab.chat.empty': 'Starte die Unterhaltung',
|
'collab.chat.empty': 'Starte die Unterhaltung',
|
||||||
'collab.chat.emptyHint': 'Nachrichten werden mit allen Reiseteilnehmern geteilt',
|
'collab.chat.emptyHint': 'Nachrichten werden mit allen Reiseteilnehmern geteilt',
|
||||||
|
'collab.chat.emptyDesc': 'Teile Ideen, Pläne und Updates mit deiner Reisegruppe',
|
||||||
'collab.chat.today': 'Heute',
|
'collab.chat.today': 'Heute',
|
||||||
'collab.chat.yesterday': 'Gestern',
|
'collab.chat.yesterday': 'Gestern',
|
||||||
'collab.chat.deletedMessage': 'hat eine Nachricht gelöscht',
|
'collab.chat.deletedMessage': 'hat eine Nachricht gelöscht',
|
||||||
|
|||||||
@@ -956,6 +956,7 @@ const en = {
|
|||||||
'collab.chat.placeholder': 'Type a message...',
|
'collab.chat.placeholder': 'Type a message...',
|
||||||
'collab.chat.empty': 'Start the conversation',
|
'collab.chat.empty': 'Start the conversation',
|
||||||
'collab.chat.emptyHint': 'Messages are shared with all trip members',
|
'collab.chat.emptyHint': 'Messages are shared with all trip members',
|
||||||
|
'collab.chat.emptyDesc': 'Share ideas, plans, and updates with your travel group',
|
||||||
'collab.chat.today': 'Today',
|
'collab.chat.today': 'Today',
|
||||||
'collab.chat.yesterday': 'Yesterday',
|
'collab.chat.yesterday': 'Yesterday',
|
||||||
'collab.chat.deletedMessage': 'deleted a message',
|
'collab.chat.deletedMessage': 'deleted a message',
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { useTranslation } from '../i18n'
|
|||||||
import { joinTrip, leaveTrip, addListener, removeListener } from '../api/websocket'
|
import { joinTrip, leaveTrip, addListener, removeListener } from '../api/websocket'
|
||||||
import { addonsApi, accommodationsApi, authApi, tripsApi, assignmentsApi } from '../api/client'
|
import { addonsApi, accommodationsApi, authApi, tripsApi, assignmentsApi } from '../api/client'
|
||||||
import { calculateRoute } from '../components/Map/RouteCalculator'
|
import { calculateRoute } from '../components/Map/RouteCalculator'
|
||||||
|
import ConfirmDialog from '../components/shared/ConfirmDialog'
|
||||||
|
|
||||||
const MIN_SIDEBAR = 200
|
const MIN_SIDEBAR = 200
|
||||||
const MAX_SIDEBAR = 520
|
const MAX_SIDEBAR = 520
|
||||||
@@ -111,6 +112,7 @@ export default function TripPlannerPage() {
|
|||||||
const [routeSegments, setRouteSegments] = useState([]) // { from, to, walkingText, drivingText }
|
const [routeSegments, setRouteSegments] = useState([]) // { from, to, walkingText, drivingText }
|
||||||
const [fitKey, setFitKey] = useState(0)
|
const [fitKey, setFitKey] = useState(0)
|
||||||
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(null) // 'left' | 'right' | null
|
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(null) // 'left' | 'right' | null
|
||||||
|
const [deletePlaceId, setDeletePlaceId] = useState(null)
|
||||||
|
|
||||||
// Load trip + files (needed for place inspector file section)
|
// Load trip + files (needed for place inspector file section)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -278,14 +280,18 @@ export default function TripPlannerPage() {
|
|||||||
}
|
}
|
||||||
}, [editingPlace, editingAssignmentId, tripId, tripStore, toast])
|
}, [editingPlace, editingAssignmentId, tripId, tripStore, toast])
|
||||||
|
|
||||||
const handleDeletePlace = useCallback(async (placeId) => {
|
const handleDeletePlace = useCallback((placeId) => {
|
||||||
if (!confirm(t('trip.confirm.deletePlace'))) return
|
setDeletePlaceId(placeId)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const confirmDeletePlace = useCallback(async () => {
|
||||||
|
if (!deletePlaceId) return
|
||||||
try {
|
try {
|
||||||
await tripStore.deletePlace(tripId, placeId)
|
await tripStore.deletePlace(tripId, deletePlaceId)
|
||||||
if (selectedPlaceId === placeId) setSelectedPlaceId(null)
|
if (selectedPlaceId === deletePlaceId) setSelectedPlaceId(null)
|
||||||
toast.success(t('trip.toast.placeDeleted'))
|
toast.success(t('trip.toast.placeDeleted'))
|
||||||
} catch (err) { toast.error(err.message) }
|
} catch (err) { toast.error(err.message) }
|
||||||
}, [tripId, tripStore, toast, selectedPlaceId])
|
}, [deletePlaceId, tripId, tripStore, toast, selectedPlaceId])
|
||||||
|
|
||||||
const handleAssignToDay = useCallback(async (placeId, dayId, position) => {
|
const handleAssignToDay = useCallback(async (placeId, dayId, position) => {
|
||||||
const target = dayId || selectedDayId
|
const target = dayId || selectedDayId
|
||||||
@@ -650,6 +656,7 @@ export default function TripPlannerPage() {
|
|||||||
}))
|
}))
|
||||||
} catch {}
|
} catch {}
|
||||||
}}
|
}}
|
||||||
|
onUpdatePlace={async (placeId, data) => { try { await tripStore.updatePlace(tripId, placeId, data) } catch (err) { toast.error(err.message) } }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -729,6 +736,13 @@ export default function TripPlannerPage() {
|
|||||||
<TripFormModal isOpen={showTripForm} onClose={() => setShowTripForm(false)} onSave={async (data) => { await tripStore.updateTrip(tripId, data); toast.success(t('trip.toast.tripUpdated')) }} trip={trip} />
|
<TripFormModal isOpen={showTripForm} onClose={() => setShowTripForm(false)} onSave={async (data) => { await tripStore.updateTrip(tripId, data); toast.success(t('trip.toast.tripUpdated')) }} trip={trip} />
|
||||||
<TripMembersModal isOpen={showMembersModal} onClose={() => setShowMembersModal(false)} tripId={tripId} tripTitle={trip?.title} />
|
<TripMembersModal isOpen={showMembersModal} onClose={() => setShowMembersModal(false)} tripId={tripId} tripTitle={trip?.title} />
|
||||||
<ReservationModal isOpen={showReservationModal} onClose={() => { setShowReservationModal(false); setEditingReservation(null) }} onSave={handleSaveReservation} reservation={editingReservation} days={days} places={places} assignments={assignments} selectedDayId={selectedDayId} files={files} onFileUpload={(fd) => tripStore.addFile(tripId, fd)} onFileDelete={(id) => tripStore.deleteFile(tripId, id)} />
|
<ReservationModal isOpen={showReservationModal} onClose={() => { setShowReservationModal(false); setEditingReservation(null) }} onSave={handleSaveReservation} reservation={editingReservation} days={days} places={places} assignments={assignments} selectedDayId={selectedDayId} files={files} onFileUpload={(fd) => tripStore.addFile(tripId, fd)} onFileDelete={(id) => tripStore.deleteFile(tripId, id)} />
|
||||||
|
<ConfirmDialog
|
||||||
|
isOpen={!!deletePlaceId}
|
||||||
|
onClose={() => setDeletePlaceId(null)}
|
||||||
|
onConfirm={confirmDeletePlace}
|
||||||
|
title={t('common.delete')}
|
||||||
|
message={t('trip.confirm.deletePlace')}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,20 +46,84 @@ const TRIP_SELECT = `
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
function generateDays(tripId, startDate, endDate) {
|
function generateDays(tripId, startDate, endDate) {
|
||||||
db.prepare('DELETE FROM days WHERE trip_id = ?').run(tripId);
|
const existing = db.prepare('SELECT id, day_number, date FROM days WHERE trip_id = ?').all(tripId);
|
||||||
|
|
||||||
if (!startDate || !endDate) {
|
if (!startDate || !endDate) {
|
||||||
const insert = db.prepare('INSERT INTO days (trip_id, day_number, date) VALUES (?, ?, NULL)');
|
// No dates — keep up to 7 dateless days, reuse existing ones
|
||||||
for (let i = 1; i <= 7; i++) insert.run(tripId, i);
|
const datelessExisting = existing.filter(d => !d.date).sort((a, b) => a.day_number - b.day_number);
|
||||||
|
// Remove days with dates (they no longer apply)
|
||||||
|
const withDates = existing.filter(d => d.date);
|
||||||
|
if (withDates.length > 0) {
|
||||||
|
db.prepare(`DELETE FROM days WHERE trip_id = ? AND date IS NOT NULL`).run(tripId);
|
||||||
|
}
|
||||||
|
// Ensure exactly 7 dateless days
|
||||||
|
const needed = 7 - datelessExisting.length;
|
||||||
|
if (needed > 0) {
|
||||||
|
const insert = db.prepare('INSERT INTO days (trip_id, day_number, date) VALUES (?, ?, NULL)');
|
||||||
|
for (let i = 0; i < needed; i++) insert.run(tripId, datelessExisting.length + i + 1);
|
||||||
|
} else if (needed < 0) {
|
||||||
|
// Too many dateless days — remove extras (highest day_number first to preserve earlier assignments)
|
||||||
|
const toRemove = datelessExisting.slice(7);
|
||||||
|
const del = db.prepare('DELETE FROM days WHERE id = ?');
|
||||||
|
for (const d of toRemove) del.run(d.id);
|
||||||
|
}
|
||||||
|
// Renumber — use negative temp values first to avoid UNIQUE conflicts
|
||||||
|
const remaining = db.prepare('SELECT id FROM days WHERE trip_id = ? ORDER BY day_number').all(tripId);
|
||||||
|
const tmpUpd = db.prepare('UPDATE days SET day_number = ? WHERE id = ?');
|
||||||
|
remaining.forEach((d, i) => tmpUpd.run(-(i + 1), d.id));
|
||||||
|
remaining.forEach((d, i) => tmpUpd.run(i + 1, d.id));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const start = new Date(startDate);
|
|
||||||
const end = new Date(endDate);
|
// Use pure string-based date math to avoid timezone/DST issues
|
||||||
const numDays = Math.min(Math.floor((end - start) / 86400000) + 1, 90);
|
const [sy, sm, sd] = startDate.split('-').map(Number);
|
||||||
const insert = db.prepare('INSERT INTO days (trip_id, day_number, date) VALUES (?, ?, ?)');
|
const [ey, em, ed] = endDate.split('-').map(Number);
|
||||||
|
const startMs = Date.UTC(sy, sm - 1, sd);
|
||||||
|
const endMs = Date.UTC(ey, em - 1, ed);
|
||||||
|
const numDays = Math.min(Math.floor((endMs - startMs) / 86400000) + 1, 90);
|
||||||
|
|
||||||
|
// Build target dates
|
||||||
|
const targetDates = [];
|
||||||
for (let i = 0; i < numDays; i++) {
|
for (let i = 0; i < numDays; i++) {
|
||||||
const d = new Date(start);
|
const d = new Date(startMs + i * 86400000);
|
||||||
d.setDate(start.getDate() + i);
|
const yyyy = d.getUTCFullYear();
|
||||||
insert.run(tripId, i + 1, d.toISOString().split('T')[0]);
|
const mm = String(d.getUTCMonth() + 1).padStart(2, '0');
|
||||||
|
const dd = String(d.getUTCDate()).padStart(2, '0');
|
||||||
|
targetDates.push(`${yyyy}-${mm}-${dd}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Index existing days by date
|
||||||
|
const existingByDate = new Map();
|
||||||
|
for (const d of existing) {
|
||||||
|
if (d.date) existingByDate.set(d.date, d);
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetDateSet = new Set(targetDates);
|
||||||
|
|
||||||
|
// Delete days whose date is no longer in the new range
|
||||||
|
const toDelete = existing.filter(d => d.date && !targetDateSet.has(d.date));
|
||||||
|
// Also delete dateless days (they are replaced by dated ones)
|
||||||
|
const datelessToDelete = existing.filter(d => !d.date);
|
||||||
|
const del = db.prepare('DELETE FROM days WHERE id = ?');
|
||||||
|
for (const d of [...toDelete, ...datelessToDelete]) del.run(d.id);
|
||||||
|
|
||||||
|
// Move all kept days to negative day_numbers to avoid UNIQUE conflicts
|
||||||
|
const setTemp = db.prepare('UPDATE days SET day_number = ? WHERE id = ?');
|
||||||
|
const kept = existing.filter(d => d.date && targetDateSet.has(d.date));
|
||||||
|
for (let i = 0; i < kept.length; i++) setTemp.run(-(i + 1), kept[i].id);
|
||||||
|
|
||||||
|
// Now assign correct day_numbers and insert missing days
|
||||||
|
const insert = db.prepare('INSERT INTO days (trip_id, day_number, date) VALUES (?, ?, ?)');
|
||||||
|
const update = db.prepare('UPDATE days SET day_number = ? WHERE id = ?');
|
||||||
|
|
||||||
|
for (let i = 0; i < targetDates.length; i++) {
|
||||||
|
const date = targetDates[i];
|
||||||
|
const ex = existingByDate.get(date);
|
||||||
|
if (ex) {
|
||||||
|
update.run(i + 1, ex.id);
|
||||||
|
} else {
|
||||||
|
insert.run(tripId, i + 1, date);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user