Add drag & drop reordering of trips on the dashboard

- trip_members.sort_order (per-user, migrated) drives GET /api/trips order
- PATCH /api/trips/:id/order updates only the caller's membership row
- Trip cards get a drag handle reusing the generalized row-reorder helper;
  clicking the card still navigates
This commit is contained in:
2026-07-20 09:41:43 +07:00
parent d393e16ae1
commit b452430415
8 changed files with 163 additions and 27 deletions
+2
View File
@@ -23,6 +23,7 @@ CREATE TABLE IF NOT EXISTS trip_members (
trip_id INTEGER NOT NULL REFERENCES trips(id),
user_id INTEGER NOT NULL REFERENCES users(id),
role TEXT NOT NULL DEFAULT 'editor',
sort_order INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (trip_id, user_id)
);
@@ -72,6 +73,7 @@ const MIGRATIONS = [
{ table: 'entries', column: 'rental', ddl: 'ALTER TABLE entries ADD COLUMN rental TEXT' },
{ table: 'entries', column: 'end_date', ddl: 'ALTER TABLE entries ADD COLUMN end_date TEXT' },
{ table: 'entries', column: 'transport_mode', ddl: 'ALTER TABLE entries ADD COLUMN transport_mode TEXT' },
{ table: 'trip_members', column: 'sort_order', ddl: 'ALTER TABLE trip_members ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0' },
];
function applyMigrations(db) {
+17 -2
View File
@@ -116,13 +116,16 @@ export default function tripsRoutes(db) {
const router = express.Router();
const listForUser = db.prepare(`
SELECT t.id, t.name, t.start_date, t.end_date, t.owner_id, t.currency, tm.role,
SELECT t.id, t.name, t.start_date, t.end_date, t.owner_id, t.currency, tm.role, tm.sort_order,
(SELECT COUNT(*) FROM trip_members WHERE trip_id = t.id) AS member_count,
(SELECT COUNT(*) FROM entries WHERE trip_id = t.id) AS entry_count
FROM trips t
JOIN trip_members tm ON tm.trip_id = t.id AND tm.user_id = ?
ORDER BY t.created_at DESC, t.id DESC
ORDER BY tm.sort_order, t.created_at DESC, t.id DESC
`);
const updateMemberOrder = db.prepare(
'UPDATE trip_members SET sort_order = ? WHERE trip_id = ? AND user_id = ?'
);
const insertTrip = db.prepare(
'INSERT INTO trips (name, start_date, end_date, owner_id, currency, join_code) VALUES (?, ?, ?, ?, ?, ?)'
);
@@ -233,6 +236,18 @@ export default function tripsRoutes(db) {
res.status(200).json({ trip: tripJson(getTrip.get(ctx.tripId)) });
});
// PATCH /api/trips/:id/order { sort_order } — caller's own dashboard order.
router.patch('/:id/order', (req, res) => {
const ctx = requireMember(req, res);
if (!ctx) return;
const { sort_order } = req.body || {};
if (!Number.isInteger(sort_order)) {
return res.status(400).json({ error: 'sort_order must be an integer' });
}
updateMemberOrder.run(sort_order, ctx.tripId, req.session.userId);
res.status(200).json({ sort_order });
});
// DELETE /api/trips/:id (owner only)
router.delete('/:id', (req, res) => {
const ctx = requireMember(req, res);