mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 13:21:46 +00:00
fix(journey): make sort_order authoritative for within-day entry ordering
Reorder buttons appeared broken because the server ORDER BY put entry_time before sort_order, so entries synced from trip places with differing times would always sort by time regardless of sort_order writes. The client store mirrored the same comparator, making even the optimistic update invisible. - Change ORDER BY to (entry_date, sort_order, id) in getJourneyFull and listEntries - Fix syncTripPlaces and onPlaceCreated to assign MAX+1 sort_order per day instead of day_number/0 - Update client store comparator to match - Add DB migration to backfill sort_order using old effective key (entry_time, id) so existing journeys retain their visual order - Add tests: JOURNEY-SVC-089–093, FE-STORE-JOURNEY-018–019 Closes #846
This commit is contained in:
@@ -2107,6 +2107,29 @@ function runMigrations(db: Database.Database): void {
|
||||
!= substr(reservations.reservation_time, 1, 10)
|
||||
`);
|
||||
},
|
||||
// #846: make sort_order authoritative within a day. Previous ORDER BY put
|
||||
// entry_time before sort_order, silently ignoring reorder clicks when two
|
||||
// same-date entries had different times. Backfill renumbers using the old
|
||||
// effective key (entry_time ASC, id ASC) so existing journeys retain their
|
||||
// current visual order.
|
||||
() => {
|
||||
db.exec(`
|
||||
WITH ranked AS (
|
||||
SELECT id,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY journey_id, entry_date
|
||||
ORDER BY entry_time ASC, id ASC
|
||||
) - 1 AS rn
|
||||
FROM journey_entries
|
||||
)
|
||||
UPDATE journey_entries
|
||||
SET sort_order = (SELECT rn FROM ranked WHERE ranked.id = journey_entries.id)
|
||||
`);
|
||||
db.exec(
|
||||
'CREATE INDEX IF NOT EXISTS idx_journey_entries_order ' +
|
||||
'ON journey_entries(journey_id, entry_date, sort_order)'
|
||||
);
|
||||
},
|
||||
];
|
||||
|
||||
if (currentVersion < migrations.length) {
|
||||
|
||||
@@ -120,7 +120,7 @@ export function getJourneyFull(journeyId: number, userId: number) {
|
||||
if (!journey) return null;
|
||||
|
||||
const entries = db.prepare(
|
||||
'SELECT * FROM journey_entries WHERE journey_id = ? ORDER BY entry_date ASC, entry_time ASC, sort_order ASC'
|
||||
'SELECT * FROM journey_entries WHERE journey_id = ? ORDER BY entry_date ASC, sort_order ASC, id ASC'
|
||||
).all(journeyId) as JourneyEntry[];
|
||||
|
||||
const photos = db.prepare(
|
||||
@@ -306,12 +306,21 @@ export function syncTripPlaces(journeyId: number, tripId: number, authorId: numb
|
||||
).all(journeyId, tripId) as { source_place_id: number }[];
|
||||
const existingPlaceIds = new Set(existing.map(e => e.source_place_id));
|
||||
|
||||
// Track next sort_order per date so synced skeletons get unique, sequential positions.
|
||||
const dateMaxOrder = new Map<string, number>();
|
||||
const maxRows = db.prepare(
|
||||
'SELECT entry_date, COALESCE(MAX(sort_order), -1) AS m FROM journey_entries WHERE journey_id = ? GROUP BY entry_date'
|
||||
).all(journeyId) as { entry_date: string; m: number }[];
|
||||
for (const row of maxRows) dateMaxOrder.set(row.entry_date, row.m);
|
||||
|
||||
for (const place of places) {
|
||||
if (existingPlaceIds.has(place.id)) continue;
|
||||
existingPlaceIds.add(place.id);
|
||||
|
||||
const entryDate = place.day_date || new Date().toISOString().split('T')[0];
|
||||
const entryTime = place.assignment_time || place.place_time || null;
|
||||
const nextOrder = (dateMaxOrder.get(entryDate) ?? -1) + 1;
|
||||
dateMaxOrder.set(entryDate, nextOrder);
|
||||
|
||||
db.prepare(`
|
||||
INSERT INTO journey_entries (journey_id, source_trip_id, source_place_id, author_id, type, title, entry_date, entry_time, location_name, location_lat, location_lng, sort_order, created_at, updated_at)
|
||||
@@ -320,7 +329,7 @@ export function syncTripPlaces(journeyId: number, tripId: number, authorId: numb
|
||||
journeyId, tripId, place.id, authorId,
|
||||
place.name, entryDate, entryTime,
|
||||
place.address || place.name, place.lat || null, place.lng || null,
|
||||
place.day_number || 0, now, now
|
||||
nextOrder, now, now
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -367,15 +376,19 @@ export function onPlaceCreated(tripId: number, placeId: number) {
|
||||
|
||||
const journey = db.prepare('SELECT user_id FROM journeys WHERE id = ?').get(link.journey_id) as { user_id: number };
|
||||
const entryDate = place.day_date;
|
||||
const maxOrder = db.prepare(
|
||||
'SELECT MAX(sort_order) AS m FROM journey_entries WHERE journey_id = ? AND entry_date = ?'
|
||||
).get(link.journey_id, entryDate) as { m: number | null };
|
||||
const nextOrder = (maxOrder?.m ?? -1) + 1;
|
||||
|
||||
db.prepare(`
|
||||
INSERT INTO journey_entries (journey_id, source_trip_id, source_place_id, author_id, type, title, entry_date, entry_time, location_name, location_lat, location_lng, sort_order, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, 'skeleton', ?, ?, ?, ?, ?, ?, 0, ?, ?)
|
||||
VALUES (?, ?, ?, ?, 'skeleton', ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
link.journey_id, tripId, placeId, journey.user_id,
|
||||
place.name, entryDate, place.assignment_time || place.place_time || null,
|
||||
place.address || place.name, place.lat || null, place.lng || null,
|
||||
now, now
|
||||
nextOrder, now, now
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -451,7 +464,7 @@ export function listEntries(journeyId: number, userId: number) {
|
||||
if (!canAccessJourney(journeyId, userId)) return null;
|
||||
|
||||
const entries = db.prepare(
|
||||
'SELECT * FROM journey_entries WHERE journey_id = ? ORDER BY entry_date ASC, entry_time ASC, sort_order ASC'
|
||||
'SELECT * FROM journey_entries WHERE journey_id = ? ORDER BY entry_date ASC, sort_order ASC, id ASC'
|
||||
).all(journeyId) as JourneyEntry[];
|
||||
|
||||
const photos = db.prepare(
|
||||
|
||||
Reference in New Issue
Block a user