diff --git a/server/src/services/tripService.ts b/server/src/services/tripService.ts index 5d024b7e..63ae51b7 100644 --- a/server/src/services/tripService.ts +++ b/server/src/services/tripService.ts @@ -122,12 +122,26 @@ export function generateDays(tripId: number | bigint | string, startDate: string del.run(dated[i].id); } - // Any remaining unused dateless days: keep as dateless, just renumber. + // Any remaining unused dateless days: drop the empty placeholders so day_count + // reflects the dated range, but keep ones that still hold content (assignments, + // notes, accommodations) — mirrors the dateless-path trimming above (#1083). // Base must be max(targetDates.length, dated.length) to avoid colliding with // positives already assigned by the main loop or the overflow loop above. + const isEmptyDay = db.prepare( + `SELECT NOT EXISTS (SELECT 1 FROM day_assignments da WHERE da.day_id = @id) + AND NOT EXISTS (SELECT 1 FROM day_notes dn WHERE dn.day_id = @id) + AND NOT EXISTS (SELECT 1 FROM day_accommodations dac WHERE dac.start_day_id = @id OR dac.end_day_id = @id) AS empty` + ); const maxAssigned = Math.max(targetDates.length, dated.length); + let keptDateless = 0; for (let i = datelessIdx; i < dateless.length; i++) { - setDayNumber.run(maxAssigned + (i - datelessIdx) + 1, dateless[i].id); + const empty = (isEmptyDay.get({ id: dateless[i].id }) as { empty: number }).empty; + if (empty) { + del.run(dateless[i].id); + } else { + setDayNumber.run(maxAssigned + keptDateless + 1, dateless[i].id); + keptDateless++; + } } // Final renumber to compact and eliminate any gaps/negatives diff --git a/server/tests/unit/services/tripService.test.ts b/server/tests/unit/services/tripService.test.ts index 795dbcb2..a4411b4d 100644 --- a/server/tests/unit/services/tripService.test.ts +++ b/server/tests/unit/services/tripService.test.ts @@ -242,6 +242,33 @@ describe('generateDays', () => { const nums = daysAfter.map(d => d.day_number).sort((a, b) => a - b); expect(nums).toEqual([1, 2, 3, 4, 5]); }); + + it('TRIP-SVC-017: switching a dateless trip to a shorter dated range drops empty leftover days but keeps ones with content (#1083)', () => { + const { user } = createUser(testDb); + // A 7-day trip, then cleared to dateless placeholders (day_count = 7). + const trip = createTrip(testDb, user.id, { start_date: '2025-12-01', end_date: '2025-12-07' }); + generateDays(trip.id, null, null); + const dateless = getDays(trip.id); + expect(dateless).toHaveLength(7); + expect(dateless.every(d => d.date === null)).toBe(true); + + // Give the LAST dateless day real content so it must be preserved. + const place = createPlace(testDb, trip.id); + const assignment = createDayAssignment(testDb, dateless[6].id, place.id); + + // Now set an explicit 2-day range. The first two dateless days are reused for + // the dates; the four empty leftovers must be removed, the one with content kept. + generateDays(trip.id, '2026-01-10', '2026-01-11'); + + const daysAfter = getDays(trip.id); + const dated = daysAfter.filter(d => d.date !== null); + const stillDateless = daysAfter.filter(d => d.date === null); + expect(dated.map(d => d.date)).toEqual(['2026-01-10', '2026-01-11']); + // day_count is COUNT(*) FROM days: 2 dated + 1 content-bearing dateless = 3 (not the stale 7) + expect(daysAfter).toHaveLength(3); + expect(stillDateless).toHaveLength(1); + expect(getAssignments(stillDateless[0].id)[0].id).toBe(assignment.id); + }); }); describe('exportICS', () => {