From 5c57116a68ce954a89bde7605b7beeb7127c8170 Mon Sep 17 00:00:00 2001 From: Maurice Date: Sun, 5 Apr 2026 23:26:35 +0200 Subject: [PATCH] fix(dayplan): restore time-based auto-sort for places and free reorder for untimed Timed places now auto-sort chronologically when a time is set. Untimed places can be freely dragged between timed items. Transports are inserted by time with per-day position override. Fixes regression from multi-day spanning PR that removed timed/untimed split. --- .../src/components/Planner/DayPlanSidebar.tsx | 17 +++++------ server/src/services/assignmentService.ts | 28 +++++++++++++++++++ 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/client/src/components/Planner/DayPlanSidebar.tsx b/client/src/components/Planner/DayPlanSidebar.tsx index 9a575be..5393ec9 100644 --- a/client/src/components/Planner/DayPlanSidebar.tsx +++ b/client/src/components/Planner/DayPlanSidebar.tsx @@ -341,14 +341,13 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({ initTransportPositions(dayId) } - // Build base list: ALL places (timed and untimed) + notes sorted by order_index/sort_order - // Places keep their order_index ordering — only transports are inserted based on time. + // All places keep their order_index — untimed can be freely moved, timed auto-sort when time is set const baseItems = [ ...da.map(a => ({ type: 'place' as const, sortKey: a.order_index, data: a })), ...dn.map(n => ({ type: 'note' as const, sortKey: n.sort_order, data: n })), ].sort((a, b) => a.sortKey - b.sortKey) - // Only transports are inserted among base items based on time/position + // Transports are inserted among places based on time const timedTransports = transport.map(r => ({ type: 'transport' as const, data: r, @@ -360,22 +359,20 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({ return timedTransports.map((item, i) => ({ ...item, sortKey: i })) } - // Insert transports among base items using persisted position or time-to-position mapping. + // Insert transports among places based on per-day position or time const result = [...baseItems] for (let ti = 0; ti < timedTransports.length; ti++) { const timed = timedTransports[ti] const minutes = timed.minutes - // Use per-day position if available, fallback to global position - const dayObj = days.find(d => d.id === dayId) + // Use per-day position if explicitly set by user reorder 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 }) + if (perDayPos != null) { + result.push({ type: timed.type, sortKey: perDayPos, data: timed.data }) continue } - // Find insertion position: after the last base item with time <= this transport's time + // Find insertion position: after the last place with time <= this transport's time let insertAfterKey = -Infinity for (const item of result) { if (item.type === 'place') { diff --git a/server/src/services/assignmentService.ts b/server/src/services/assignmentService.ts index 72ac764..b7579ea 100644 --- a/server/src/services/assignmentService.ts +++ b/server/src/services/assignmentService.ts @@ -168,6 +168,34 @@ export function getParticipants(assignmentId: string | number) { export function updateTime(id: string | number, placeTime: string | null, endTime: string | null) { db.prepare('UPDATE day_assignments SET assignment_time = ?, assignment_end_time = ? WHERE id = ?') .run(placeTime ?? null, endTime ?? null, id); + + // Auto-sort: reorder timed assignments chronologically within the day + if (placeTime) { + const assignment = db.prepare('SELECT day_id FROM day_assignments WHERE id = ?').get(id) as { day_id: number } | undefined; + if (assignment) { + const dayAssignments = db.prepare(` + SELECT da.id, COALESCE(da.assignment_time, p.place_time) as effective_time + FROM day_assignments da + JOIN places p ON da.place_id = p.id + WHERE da.day_id = ? + ORDER BY da.order_index ASC + `).all(assignment.day_id) as { id: number; effective_time: string | null }[]; + + // Separate timed and untimed, sort timed by time + const timed = dayAssignments.filter(a => a.effective_time).sort((a, b) => { + const ta = a.effective_time!.includes(':') ? a.effective_time! : '99:99'; + const tb = b.effective_time!.includes(':') ? b.effective_time! : '99:99'; + return ta.localeCompare(tb); + }); + const untimed = dayAssignments.filter(a => !a.effective_time); + + // Interleave: timed in chronological order, untimed keep relative position + const reordered = [...timed, ...untimed]; + const update = db.prepare('UPDATE day_assignments SET order_index = ? WHERE id = ?'); + reordered.forEach((a, i) => update.run(i, a.id)); + } + } + return getAssignmentWithPlace(Number(id)); }