Per-assignment times, participant avatar fix, UI improvements

- Times are now per-assignment instead of per-place, so the same place
  on different days can have different times
- Migration 26 adds assignment_time/assignment_end_time columns
- New endpoint PUT /assignments/:id/time for updating assignment times
- Time picker removed from place creation (only shown when editing)
- End-before-start validation disables save button
- Time collision warning shows overlapping activities on the same day
- Fix participant avatars using avatar_url instead of avatar filename
- Rename "Add Place" to "Add Place/Activity" (DE + EN)
- Improve README update instructions with docker inspect tip
This commit is contained in:
Maurice
2026-03-25 16:47:10 +01:00
parent 66e2799870
commit 3bf49d4180
14 changed files with 191 additions and 56 deletions

View File

@@ -556,6 +556,19 @@ function initDb() {
_db.prepare("INSERT OR IGNORE INTO addons (id, name, description, type, icon, enabled, sort_order) VALUES ('collab', 'Collab', 'Notes, polls, and live chat for trip collaboration', 'trip', 'Users', 0, 6)").run();
} catch {}
},
// 26: Per-assignment times (instead of shared place times)
() => {
try { _db.exec('ALTER TABLE day_assignments ADD COLUMN assignment_time TEXT'); } catch {}
try { _db.exec('ALTER TABLE day_assignments ADD COLUMN assignment_end_time TEXT'); } catch {}
// Copy existing place times to assignments as initial values
try {
_db.exec(`
UPDATE day_assignments SET
assignment_time = (SELECT place_time FROM places WHERE places.id = day_assignments.place_id),
assignment_end_time = (SELECT end_time FROM places WHERE places.id = day_assignments.place_id)
`);
} catch {}
},
// Future migrations go here (append only, never reorder)
];

View File

@@ -13,7 +13,9 @@ function getAssignmentWithPlace(assignmentId) {
const a = db.prepare(`
SELECT da.*, p.id as place_id, p.name as place_name, p.description as place_description,
p.lat, p.lng, p.address, p.category_id, p.price, p.currency as place_currency,
p.place_time, p.end_time, p.duration_minutes, p.notes as place_notes,
COALESCE(da.assignment_time, p.place_time) as place_time,
COALESCE(da.assignment_end_time, p.end_time) as end_time,
p.duration_minutes, p.notes as place_notes,
p.image_url, p.transport_mode, p.google_place_id, p.website, p.phone,
c.name as category_name, c.color as category_color, c.icon as category_icon
FROM day_assignments da
@@ -87,7 +89,9 @@ router.get('/trips/:tripId/days/:dayId/assignments', authenticate, (req, res) =>
const assignments = db.prepare(`
SELECT da.*, p.id as place_id, p.name as place_name, p.description as place_description,
p.lat, p.lng, p.address, p.category_id, p.price, p.currency as place_currency,
p.place_time, p.end_time, p.duration_minutes, p.notes as place_notes,
COALESCE(da.assignment_time, p.place_time) as place_time,
COALESCE(da.assignment_end_time, p.end_time) as end_time,
p.duration_minutes, p.notes as place_notes,
p.image_url, p.transport_mode, p.google_place_id, p.website, p.phone,
c.name as category_name, c.color as category_color, c.icon as category_icon
FROM day_assignments da
@@ -277,6 +281,27 @@ router.get('/trips/:tripId/assignments/:id/participants', authenticate, (req, re
res.json({ participants });
});
// PUT /api/trips/:tripId/assignments/:id/time — update per-assignment time
router.put('/trips/:tripId/assignments/:id/time', authenticate, (req, res) => {
const { tripId, id } = req.params;
if (!canAccessTrip(Number(tripId), req.user.id)) return res.status(404).json({ error: 'Trip not found' });
const assignment = db.prepare(`
SELECT da.* FROM day_assignments da
JOIN days d ON da.day_id = d.id
WHERE da.id = ? AND d.trip_id = ?
`).get(id, tripId);
if (!assignment) return res.status(404).json({ error: 'Assignment not found' });
const { place_time, end_time } = req.body;
db.prepare('UPDATE day_assignments SET assignment_time = ?, assignment_end_time = ? WHERE id = ?')
.run(place_time ?? null, end_time ?? null, id);
const updated = getAssignmentWithPlace(id);
res.json({ assignment: updated });
broadcast(Number(tripId), 'assignment:updated', { assignment: updated }, req.headers['x-socket-id']);
});
// PUT /api/trips/:tripId/assignments/:id/participants — set participants (replace all)
router.put('/trips/:tripId/assignments/:id/participants', authenticate, (req, res) => {
const { tripId, id } = req.params;

View File

@@ -13,7 +13,9 @@ function getAssignmentsForDay(dayId) {
const assignments = db.prepare(`
SELECT da.*, p.id as place_id, p.name as place_name, p.description as place_description,
p.lat, p.lng, p.address, p.category_id, p.price, p.currency as place_currency,
p.place_time, p.end_time, p.duration_minutes, p.notes as place_notes,
COALESCE(da.assignment_time, p.place_time) as place_time,
COALESCE(da.assignment_end_time, p.end_time) as end_time,
p.duration_minutes, p.notes as place_notes,
p.image_url, p.transport_mode, p.google_place_id, p.website, p.phone,
c.name as category_name, c.color as category_color, c.icon as category_icon
FROM day_assignments da
@@ -89,7 +91,9 @@ router.get('/', authenticate, (req, res) => {
const allAssignments = db.prepare(`
SELECT da.*, p.id as place_id, p.name as place_name, p.description as place_description,
p.lat, p.lng, p.address, p.category_id, p.price, p.currency as place_currency,
p.place_time, p.end_time, p.duration_minutes, p.notes as place_notes,
COALESCE(da.assignment_time, p.place_time) as place_time,
COALESCE(da.assignment_end_time, p.end_time) as end_time,
p.duration_minutes, p.notes as place_notes,
p.image_url, p.transport_mode, p.google_place_id, p.website, p.phone,
c.name as category_name, c.color as category_color, c.icon as category_icon
FROM day_assignments da