Reorder whole days and insert a day (#589) (#1148)

* feat(days): reorder whole days and insert a day at a position

Adds reorderDays + insertDay to the day service and a PUT /days/reorder route
(plus an optional position on create). Day rows stay stable so a day's
assignments, notes, bookings and accommodations ride along by id; on a dated
trip the calendar dates stay pinned to their slots while the content moves
across them, and each booking's date is re-stamped onto its day's new date
(time-of-day preserved) so day_id stays consistent. Renumbering uses the
two-phase write to avoid the UNIQUE(trip_id, day_number) collision, and a move
that would invert an accommodation's check-in/out span is rejected.

* feat(planner): reorder days from a toolbar popup, and add days

A new toolbar button opens a popup listing the days; drag a row by its grip or
use the up/down arrows to reorder, and add a day from there. Reorders apply
optimistically with rollback and sync over WebSocket; the day headers are left
untouched, so the existing place drop-targets are unaffected.

* i18n: add day-reorder strings across all languages
This commit is contained in:
Maurice
2026-06-12 00:17:49 +02:00
committed by GitHub
parent 1378c95078
commit f46cc8a98e
34 changed files with 872 additions and 9 deletions
+8
View File
@@ -49,9 +49,17 @@ export type Day = z.infer<typeof daySchema>;
export const dayCreateRequestSchema = z.object({
date: z.string().optional(),
notes: z.string().optional(),
// 1-based slot to insert a new empty day at (omit to append at the end).
position: z.number().int().positive().optional(),
});
export type DayCreateRequest = z.infer<typeof dayCreateRequestSchema>;
/** Reorder whole days: the desired full sequence of this trip's day ids. */
export const dayReorderRequestSchema = z.object({
orderedIds: z.array(z.number()),
});
export type DayReorderRequest = z.infer<typeof dayReorderRequestSchema>;
export const dayUpdateRequestSchema = z.object({
notes: z.string().optional(),
title: z.string().nullable().optional(),
+9
View File
@@ -45,5 +45,14 @@ const dayplan: TranslationStrings = {
'dayplan.mobile.allAssigned': 'All places assigned', // en-fallback
'dayplan.mobile.noMatch': 'No match', // en-fallback
'dayplan.mobile.createNew': 'Create new place', // en-fallback
'dayplan.reorderDays': 'إعادة ترتيب الأيام',
'dayplan.reorderTitle': 'إعادة ترتيب الأيام',
'dayplan.reorderHint': 'تنتقل أماكن اليوم وملاحظاته وحجوزاته معه.',
'dayplan.addDay': 'إضافة يوم',
'dayplan.moveUp': 'تحريك لأعلى',
'dayplan.moveDown': 'تحريك لأسفل',
'dayplan.reorderUndo': 'إعادة ترتيب الأيام',
'dayplan.reorderError': 'تعذّر إعادة ترتيب الأيام',
'dayplan.addDayError': 'تعذّر إضافة يوم',
};
export default dayplan;
+9
View File
@@ -48,5 +48,14 @@ const dayplan: TranslationStrings = {
'dayplan.mobile.createNew': 'Criar novo lugar',
'dayplan.expandAll': 'Expand all days', // en-fallback
'dayplan.collapseAll': 'Collapse all days', // en-fallback
'dayplan.reorderDays': 'Reordenar dias',
'dayplan.reorderTitle': 'Reordenar dias',
'dayplan.reorderHint': 'Os lugares, notas e reservas de um dia se movem junto com ele.',
'dayplan.addDay': 'Adicionar dia',
'dayplan.moveUp': 'Mover para cima',
'dayplan.moveDown': 'Mover para baixo',
'dayplan.reorderUndo': 'Reordenar dias',
'dayplan.reorderError': 'Falha ao reordenar os dias',
'dayplan.addDayError': 'Falha ao adicionar o dia',
};
export default dayplan;
+9
View File
@@ -48,5 +48,14 @@ const dayplan: TranslationStrings = {
'dayplan.mobile.createNew': 'Vytvořit nové místo',
'dayplan.expandAll': 'Expand all days', // en-fallback
'dayplan.collapseAll': 'Collapse all days', // en-fallback
'dayplan.reorderDays': 'Změnit pořadí dnů',
'dayplan.reorderTitle': 'Změnit pořadí dnů',
'dayplan.reorderHint': 'Místa, poznámky a rezervace daného dne se přesunou spolu s ním.',
'dayplan.addDay': 'Přidat den',
'dayplan.moveUp': 'Posunout nahoru',
'dayplan.moveDown': 'Posunout dolů',
'dayplan.reorderUndo': 'Změnit pořadí dnů',
'dayplan.reorderError': 'Nepodařilo se změnit pořadí dnů',
'dayplan.addDayError': 'Nepodařilo se přidat den',
};
export default dayplan;
+9
View File
@@ -47,5 +47,14 @@ const dayplan: TranslationStrings = {
'dayplan.mobile.allAssigned': 'Alle Orte zugeordnet',
'dayplan.mobile.noMatch': 'Kein Treffer',
'dayplan.mobile.createNew': 'Neuen Ort erstellen',
'dayplan.reorderDays': 'Tage neu anordnen',
'dayplan.reorderTitle': 'Tage neu anordnen',
'dayplan.reorderHint': 'Orte, Notizen und Buchungen eines Tages werden mitverschoben.',
'dayplan.addDay': 'Tag hinzufügen',
'dayplan.moveUp': 'Nach oben',
'dayplan.moveDown': 'Nach unten',
'dayplan.reorderUndo': 'Tage neu anordnen',
'dayplan.reorderError': 'Tage konnten nicht neu angeordnet werden',
'dayplan.addDayError': 'Tag konnte nicht hinzugefügt werden',
};
export default dayplan;
+9
View File
@@ -47,5 +47,14 @@ const dayplan: TranslationStrings = {
'dayplan.mobile.allAssigned': 'All places assigned',
'dayplan.mobile.noMatch': 'No match',
'dayplan.mobile.createNew': 'Create new place',
'dayplan.reorderDays': 'Reorder days',
'dayplan.reorderTitle': 'Reorder days',
'dayplan.reorderHint': "A day's places, notes and bookings move with it.",
'dayplan.addDay': 'Add day',
'dayplan.moveUp': 'Move up',
'dayplan.moveDown': 'Move down',
'dayplan.reorderUndo': 'Reorder days',
'dayplan.reorderError': 'Failed to reorder days',
'dayplan.addDayError': 'Failed to add day',
};
export default dayplan;
+9
View File
@@ -47,5 +47,14 @@ const dayplan: TranslationStrings = {
'dayplan.mobile.createNew': 'Crear nuevo lugar',
'dayplan.expandAll': 'Expand all days', // en-fallback
'dayplan.collapseAll': 'Collapse all days', // en-fallback
'dayplan.reorderDays': 'Reordenar días',
'dayplan.reorderTitle': 'Reordenar días',
'dayplan.reorderHint': 'Los lugares, las notas y las reservas de un día se mueven con él.',
'dayplan.addDay': 'Añadir día',
'dayplan.moveUp': 'Subir',
'dayplan.moveDown': 'Bajar',
'dayplan.reorderUndo': 'Reordenar días',
'dayplan.reorderError': 'No se pudieron reordenar los días',
'dayplan.addDayError': 'No se pudo añadir el día',
};
export default dayplan;
+9
View File
@@ -49,5 +49,14 @@ const dayplan: TranslationStrings = {
'dayplan.mobile.createNew': 'Créer un nouveau lieu',
'dayplan.expandAll': 'Expand all days', // en-fallback
'dayplan.collapseAll': 'Collapse all days', // en-fallback
'dayplan.reorderDays': 'Réorganiser les jours',
'dayplan.reorderTitle': 'Réorganiser les jours',
'dayplan.reorderHint': 'Les lieux, notes et réservations d\'un jour le suivent.',
'dayplan.addDay': 'Ajouter un jour',
'dayplan.moveUp': 'Monter',
'dayplan.moveDown': 'Descendre',
'dayplan.reorderUndo': 'Réorganiser les jours',
'dayplan.reorderError': 'Échec de la réorganisation des jours',
'dayplan.addDayError': 'Échec de l\'ajout du jour',
};
export default dayplan;
+9
View File
@@ -49,5 +49,14 @@ const dayplan: TranslationStrings = {
'dayplan.mobile.allAssigned': 'Όλες οι τοποθεσίες έχουν ανατεθεί',
'dayplan.mobile.noMatch': 'Καμία αντιστοιχία',
'dayplan.mobile.createNew': 'Δημιουργία νέας τοποθεσίας',
'dayplan.reorderDays': 'Αναδιάταξη ημερών',
'dayplan.reorderTitle': 'Αναδιάταξη ημερών',
'dayplan.reorderHint': 'Τα μέρη, οι σημειώσεις και οι κρατήσεις μιας ημέρας μετακινούνται μαζί της.',
'dayplan.addDay': 'Προσθήκη ημέρας',
'dayplan.moveUp': 'Μετακίνηση πάνω',
'dayplan.moveDown': 'Μετακίνηση κάτω',
'dayplan.reorderUndo': 'Αναδιάταξη ημερών',
'dayplan.reorderError': 'Η αναδιάταξη των ημερών απέτυχε',
'dayplan.addDayError': 'Η προσθήκη ημέρας απέτυχε',
};
export default dayplan;
+9
View File
@@ -48,5 +48,14 @@ const dayplan: TranslationStrings = {
'dayplan.mobile.createNew': 'Új helyszín létrehozása',
'dayplan.expandAll': 'Expand all days', // en-fallback
'dayplan.collapseAll': 'Collapse all days', // en-fallback
'dayplan.reorderDays': 'Napok átrendezése',
'dayplan.reorderTitle': 'Napok átrendezése',
'dayplan.reorderHint': 'A nap helyei, jegyzetei és foglalásai együtt mozognak vele.',
'dayplan.addDay': 'Nap hozzáadása',
'dayplan.moveUp': 'Mozgatás felfelé',
'dayplan.moveDown': 'Mozgatás lefelé',
'dayplan.reorderUndo': 'Napok átrendezése',
'dayplan.reorderError': 'Nem sikerült átrendezni a napokat',
'dayplan.addDayError': 'Nem sikerült napot hozzáadni',
};
export default dayplan;
+9
View File
@@ -48,5 +48,14 @@ const dayplan: TranslationStrings = {
'dayplan.mobile.createNew': 'Buat tempat baru',
'dayplan.expandAll': 'Expand all days', // en-fallback
'dayplan.collapseAll': 'Collapse all days', // en-fallback
'dayplan.reorderDays': 'Atur ulang hari',
'dayplan.reorderTitle': 'Atur ulang hari',
'dayplan.reorderHint': 'Tempat, catatan, dan pesanan pada suatu hari ikut berpindah.',
'dayplan.addDay': 'Tambah hari',
'dayplan.moveUp': 'Pindah ke atas',
'dayplan.moveDown': 'Pindah ke bawah',
'dayplan.reorderUndo': 'Atur ulang hari',
'dayplan.reorderError': 'Gagal mengatur ulang hari',
'dayplan.addDayError': 'Gagal menambah hari',
};
export default dayplan;
+9
View File
@@ -49,5 +49,14 @@ const dayplan: TranslationStrings = {
'dayplan.mobile.createNew': 'Crea nuovo luogo',
'dayplan.expandAll': 'Expand all days', // en-fallback
'dayplan.collapseAll': 'Collapse all days', // en-fallback
'dayplan.reorderDays': 'Riordina i giorni',
'dayplan.reorderTitle': 'Riordina i giorni',
'dayplan.reorderHint': 'I luoghi, le note e le prenotazioni di un giorno si spostano insieme a esso.',
'dayplan.addDay': 'Aggiungi giorno',
'dayplan.moveUp': 'Sposta su',
'dayplan.moveDown': 'Sposta giù',
'dayplan.reorderUndo': 'Riordina i giorni',
'dayplan.reorderError': 'Riordino dei giorni non riuscito',
'dayplan.addDayError': 'Aggiunta del giorno non riuscita',
};
export default dayplan;
+9
View File
@@ -42,5 +42,14 @@ const dayplan: TranslationStrings = {
'dayplan.mobile.allAssigned': 'すべて割り当て済み',
'dayplan.mobile.noMatch': '一致なし',
'dayplan.mobile.createNew': '新しい場所を作成',
'dayplan.reorderDays': '日付を並べ替え',
'dayplan.reorderTitle': '日付を並べ替え',
'dayplan.reorderHint': 'その日のスポット、メモ、予約も一緒に移動します。',
'dayplan.addDay': '日付を追加',
'dayplan.moveUp': '上へ移動',
'dayplan.moveDown': '下へ移動',
'dayplan.reorderUndo': '日付を並べ替え',
'dayplan.reorderError': '日付の並べ替えに失敗しました',
'dayplan.addDayError': '日付の追加に失敗しました',
};
export default dayplan;
+9
View File
@@ -45,5 +45,14 @@ const dayplan: TranslationStrings = {
'dayplan.mobile.allAssigned': '모든 장소가 배정되었습니다',
'dayplan.mobile.noMatch': '일치 없음',
'dayplan.mobile.createNew': '새 장소 만들기',
'dayplan.reorderDays': '날짜 순서 변경',
'dayplan.reorderTitle': '날짜 순서 변경',
'dayplan.reorderHint': '해당 날짜의 장소, 메모, 예약이 함께 이동합니다.',
'dayplan.addDay': '날짜 추가',
'dayplan.moveUp': '위로 이동',
'dayplan.moveDown': '아래로 이동',
'dayplan.reorderUndo': '날짜 순서 변경',
'dayplan.reorderError': '날짜 순서를 변경하지 못했습니다',
'dayplan.addDayError': '날짜를 추가하지 못했습니다',
};
export default dayplan;
+9
View File
@@ -48,5 +48,14 @@ const dayplan: TranslationStrings = {
'dayplan.mobile.createNew': 'Nieuwe plaats aanmaken',
'dayplan.expandAll': 'Expand all days', // en-fallback
'dayplan.collapseAll': 'Collapse all days', // en-fallback
'dayplan.reorderDays': 'Dagen herordenen',
'dayplan.reorderTitle': 'Dagen herordenen',
'dayplan.reorderHint': 'De plaatsen, notities en boekingen van een dag gaan mee.',
'dayplan.addDay': 'Dag toevoegen',
'dayplan.moveUp': 'Omhoog',
'dayplan.moveDown': 'Omlaag',
'dayplan.reorderUndo': 'Dagen herordenen',
'dayplan.reorderError': 'Dagen herordenen mislukt',
'dayplan.addDayError': 'Dag toevoegen mislukt',
};
export default dayplan;
+9
View File
@@ -48,5 +48,14 @@ const dayplan: TranslationStrings = {
'dayplan.mobile.createNew': 'Utwórz nowe miejsce',
'dayplan.expandAll': 'Expand all days', // en-fallback
'dayplan.collapseAll': 'Collapse all days', // en-fallback
'dayplan.reorderDays': 'Zmień kolejność dni',
'dayplan.reorderTitle': 'Zmień kolejność dni',
'dayplan.reorderHint': 'Miejsca, notatki i rezerwacje danego dnia przenoszą się razem z nim.',
'dayplan.addDay': 'Dodaj dzień',
'dayplan.moveUp': 'Przenieś w górę',
'dayplan.moveDown': 'Przenieś w dół',
'dayplan.reorderUndo': 'Zmień kolejność dni',
'dayplan.reorderError': 'Nie udało się zmienić kolejności dni',
'dayplan.addDayError': 'Nie udało się dodać dnia',
};
export default dayplan;
+9
View File
@@ -48,5 +48,14 @@ const dayplan: TranslationStrings = {
'dayplan.mobile.createNew': 'Создать новое место',
'dayplan.expandAll': 'Expand all days', // en-fallback
'dayplan.collapseAll': 'Collapse all days', // en-fallback
'dayplan.reorderDays': 'Изменить порядок дней',
'dayplan.reorderTitle': 'Изменить порядок дней',
'dayplan.reorderHint': 'Места, заметки и бронирования дня перемещаются вместе с ним.',
'dayplan.addDay': 'Добавить день',
'dayplan.moveUp': 'Вверх',
'dayplan.moveDown': 'Вниз',
'dayplan.reorderUndo': 'Изменить порядок дней',
'dayplan.reorderError': 'Не удалось изменить порядок дней',
'dayplan.addDayError': 'Не удалось добавить день',
};
export default dayplan;
+9
View File
@@ -47,5 +47,14 @@ const dayplan: TranslationStrings = {
'dayplan.mobile.allAssigned': 'Tüm yerler atandı',
'dayplan.mobile.noMatch': 'Eşleşme yok',
'dayplan.mobile.createNew': 'Yeni yer oluştur',
'dayplan.reorderDays': 'Günleri yeniden sırala',
'dayplan.reorderTitle': 'Günleri yeniden sırala',
'dayplan.reorderHint': 'Bir günün yerleri, notları ve rezervasyonları onunla birlikte taşınır.',
'dayplan.addDay': 'Gün ekle',
'dayplan.moveUp': 'Yukarı taşı',
'dayplan.moveDown': 'Aşağı taşı',
'dayplan.reorderUndo': 'Günleri yeniden sırala',
'dayplan.reorderError': 'Günler yeniden sıralanamadı',
'dayplan.addDayError': 'Gün eklenemedi',
};
export default dayplan;
+9
View File
@@ -47,5 +47,14 @@ const dayplan: TranslationStrings = {
'dayplan.mobile.allAssigned': 'Усі місця розподілені',
'dayplan.mobile.noMatch': 'Немає збігів',
'dayplan.mobile.createNew': 'Створити нове місце',
'dayplan.reorderDays': 'Змінити порядок днів',
'dayplan.reorderTitle': 'Змінити порядок днів',
'dayplan.reorderHint': 'Місця, нотатки та бронювання дня переміщуються разом із ним.',
'dayplan.addDay': 'Додати день',
'dayplan.moveUp': 'Перемістити вгору',
'dayplan.moveDown': 'Перемістити вниз',
'dayplan.reorderUndo': 'Змінити порядок днів',
'dayplan.reorderError': 'Не вдалося змінити порядок днів',
'dayplan.addDayError': 'Не вдалося додати день',
};
export default dayplan;
+9
View File
@@ -42,5 +42,14 @@ const dayplan: TranslationStrings = {
'dayplan.mobile.createNew': '建立新地點',
'dayplan.expandAll': 'Expand all days', // en-fallback
'dayplan.collapseAll': 'Collapse all days', // en-fallback
'dayplan.reorderDays': '重新排序日期',
'dayplan.reorderTitle': '重新排序日期',
'dayplan.reorderHint': '該日的地點、筆記和預訂都會一併移動。',
'dayplan.addDay': '新增日期',
'dayplan.moveUp': '上移',
'dayplan.moveDown': '下移',
'dayplan.reorderUndo': '重新排序日期',
'dayplan.reorderError': '重新排序日期失敗',
'dayplan.addDayError': '新增日期失敗',
};
export default dayplan;
+9
View File
@@ -42,5 +42,14 @@ const dayplan: TranslationStrings = {
'dayplan.mobile.createNew': '创建新地点',
'dayplan.expandAll': 'Expand all days', // en-fallback
'dayplan.collapseAll': 'Collapse all days', // en-fallback
'dayplan.reorderDays': '调整日期顺序',
'dayplan.reorderTitle': '调整日期顺序',
'dayplan.reorderHint': '该天的地点、备注和预订会随之一起移动。',
'dayplan.addDay': '添加一天',
'dayplan.moveUp': '上移',
'dayplan.moveDown': '下移',
'dayplan.reorderUndo': '调整日期顺序',
'dayplan.reorderError': '调整日期顺序失败',
'dayplan.addDayError': '添加一天失败',
};
export default dayplan;