feat(trips): shortening a trip says first which days go and what is on them

The trip dialog on the desktop and the trip sheet on a phone now ask before a save whose new dates remove days with something on them. The question names those days the way the day list does and lists what goes with them in the same rows the delete day question uses: planned places stay in the place list, notes and day texts are deleted, bookings stay under Bookings, and a stay that checks in or out on a removed day goes as a whole while its booking and expense remain, shown in the danger tone. A moved start gets both questions in one step, with the booking hint following the chosen shift mode, and a save that only drops empty days asks nothing.

The rule that lays the days out moved into shared as planDayGrid, with resolveDayGridRange deciding whether the rows are rebuilt at all. generateDays on the server now carries that plan out in one transaction, so the warning names exactly the days the save takes, and a seeded fuzz test holds the new rebuild to the old one over 300 random day grids. The update_trip tool says in its description what shortening takes and lists removed_days in its answer.

Alongside, the dialog sends a day count only when it changed, so a stale count no longer trims a day just added, stays are reloaded after every date change including the one made on a phone, and the date shift tip now points to adding a day under Reorder days.
This commit is contained in:
Maurice
2026-09-23 19:54:09 +02:00
parent b56f335978
commit 1b63285ad0
50 changed files with 2122 additions and 231 deletions
+16 -1
View File
@@ -95,7 +95,22 @@ const dashboard: TranslationStrings = {
'تنتقل خطط الأيام مع التواريخ الجديدة، بينما تبقى الحجوزات وأماكن الإقامة في تواريخها الأصلية ما دامت تلك التواريخ لا تزال ضمن الرحلة.',
'dashboard.dateShiftAll': 'نقل كل شيء',
'dashboard.dateShiftAllDesc': 'ينتقل خط سير الرحلة بالكامل مع التواريخ الجديدة، بما في ذلك الحجوزات وأماكن الإقامة.',
'dashboard.dateShiftHint': 'نصيحة: لنقل جزء من خط سير الرحلة فقط، استخدم "إضافة يوم" في مخطط الرحلة بدلاً من ذلك.',
'dashboard.dateShiftHint':
'نصيحة: لنقل جزء من خط سير الرحلة فقط، أضف يومًا من "إعادة ترتيب الأيام" في مخطط الرحلة ثم انقله إلى المكان الذي يجب أن تكون فيه الفجوة.',
'dashboard.shrinkTitle': 'إزالة أيام؟',
'dashboard.shrinkIntro': 'عند حفظ التواريخ الجديدة تُزال هذه الأيام:',
'dashboard.shrinkMoreDays': '+{count} أخرى',
'dashboard.shrinkLastDays': 'تُزال الأيام الأخيرة، لا الأولى',
'dashboard.shrinkLastDaysHint':
'تنتقل خطط الأيام مع التواريخ الجديدة، لذا تُزال دائمًا الأيام الأخيرة من خطتك، حتى عندما تغيّر تاريخ البداية.',
'dashboard.shrinkBookingsHint': 'تبقى ضمن الحجوزات. والحجز الذي لا يزال تاريخه ضمن الرحلة يعود إلى ذلك اليوم.',
'dashboard.shrinkBookingsShiftHint': 'تبقى ضمن الحجوزات، دون يوم.',
'dashboard.shrinkStayHint': 'يقع تسجيل الوصول أو المغادرة في يوم تتم إزالته، لذا تُزال الإقامة بالكامل.',
'dashboard.shrinkStayBookedHint':
'يقع تسجيل الوصول أو المغادرة في يوم تتم إزالته، لذا تُزال الإقامة بالكامل. ويبقى حجزها "{booking}" ومصروفه ضمن الحجوزات.',
'dashboard.shrinkConfirm': 'إزالة الأيام والحفظ',
'dashboard.shrinkUnknown':
'تعذّر التحقق من أيام هذه الرحلة. إذا كانت التواريخ الجديدة تضم أيامًا أقل، فإن الحفظ يزيل الأيام الأخيرة وكل ما خُطط فيها.',
'dashboard.filter.planned': 'مخطط لها',
'dashboard.hero.badgeLive': 'مباشر الآن',
'dashboard.hero.badgeToday': 'تبدأ اليوم',
+17 -1
View File
@@ -95,7 +95,23 @@ const dashboard: TranslationStrings = {
'Os planos diários acompanham as novas datas, enquanto reservas e hospedagens permanecem nas datas originais, desde que ainda façam parte da viagem.',
'dashboard.dateShiftAll': 'Mover tudo',
'dashboard.dateShiftAllDesc': 'Todo o itinerário acompanha as novas datas, incluindo reservas e hospedagens.',
'dashboard.dateShiftHint': 'Dica: para mover apenas parte do itinerário, use "Adicionar dia" no planejador.',
'dashboard.dateShiftHint':
'Dica: para mover apenas parte do itinerário, adicione um dia em “Reordenar dias” no planejador e mova-o para onde a lacuna deve ficar.',
'dashboard.shrinkTitle': 'Remover dias?',
'dashboard.shrinkIntro': 'Ao salvar as novas datas, estes dias serão removidos:',
'dashboard.shrinkMoreDays': '+{count} outros',
'dashboard.shrinkLastDays': 'Saem os últimos dias, não os primeiros',
'dashboard.shrinkLastDaysHint':
'Os planos de cada dia acompanham as novas datas, por isso são sempre os últimos dias do plano que saem, mesmo quando o início mudou.',
'dashboard.shrinkBookingsHint':
'Elas continuam em Reservas. Uma cuja data ainda faz parte da viagem volta para esse dia.',
'dashboard.shrinkBookingsShiftHint': 'Elas continuam em Reservas, sem dia.',
'dashboard.shrinkStayHint': 'O check-in ou o check-out é em um dia removido, então a hospedagem inteira é removida.',
'dashboard.shrinkStayBookedHint':
'O check-in ou o check-out é em um dia removido, então a hospedagem inteira é removida. A reserva “{booking}” e a despesa dela continuam em Reservas.',
'dashboard.shrinkConfirm': 'Remover dias e salvar',
'dashboard.shrinkUnknown':
'Não foi possível verificar os dias desta viagem. Se as novas datas tiverem menos dias, salvar remove os últimos dias e tudo o que está planejado neles.',
'dashboard.greeting.morning': 'Bom dia,',
'dashboard.greeting.afternoon': 'Boa tarde,',
'dashboard.greeting.evening': 'Boa noite,',
+16 -1
View File
@@ -181,7 +181,22 @@ const dashboard: TranslationStrings = {
'dashboard.dateShiftAll': 'Mou-ho tot',
'dashboard.dateShiftAllDesc': "Tot l'itinerari es mou amb les noves dates, incloses les reserves i els allotjaments.",
'dashboard.dateShiftHint':
'Consell: per moure només una part de l\'itinerari, fes servir "Afegeix un dia" al planificador.',
"Consell: per moure només una part de l'itinerari, afegeix un dia a «Reordena els dies» del planificador i mou-lo on ha de quedar el buit.",
'dashboard.shrinkTitle': 'Vols treure dies?',
'dashboard.shrinkIntro': 'En desar les noves dates es treuen aquests dies:',
'dashboard.shrinkMoreDays': '+{count} més',
'dashboard.shrinkLastDays': 'Es treuen els últims dies, no els primers',
'dashboard.shrinkLastDaysHint':
"Els plans de cada dia es mouen amb les noves dates, així que sempre es treuen els últims dies del pla, també quan ha canviat l'inici.",
'dashboard.shrinkBookingsHint':
'Es queden a Reserves. Les que tenen una data que encara és dins del viatge tornen a aquell dia.',
'dashboard.shrinkBookingsShiftHint': 'Es queden a Reserves, sense dia.',
'dashboard.shrinkStayHint': "L'entrada o la sortida és un dia que es treu, així que es treu tota l'estada.",
'dashboard.shrinkStayBookedHint':
"L'entrada o la sortida és un dia que es treu, així que es treu tota l'estada. La seva reserva «{booking}» i la seva despesa es queden a Reserves.",
'dashboard.shrinkConfirm': 'Treu els dies i desa',
'dashboard.shrinkUnknown':
"No s'han pogut comprovar els dies d'aquest viatge. Si les noves dates tenen menys dies, en desar es treuen els últims dies i tot el que hi ha planificat.",
'dashboard.subscribeAllTrips': 'Subscriu-te a tots els viatges',
'dashboard.subscribeAllTripsDesc':
'Un sol canal de calendari per a tots els teus viatges actius, sincronitzat automàticament. Exclou els viatges arxivats i els que van acabar fa més de 90 dies.',
+16 -1
View File
@@ -96,7 +96,22 @@ const dashboard: TranslationStrings = {
'dashboard.dateShiftAll': 'Posunout vše',
'dashboard.dateShiftAllDesc': 'Celý itinerář se posune podle nových termínů, včetně rezervací a ubytování.',
'dashboard.dateShiftHint':
'Tip: pokud chcete posunout jen část itineráře, použijte místo toho „Přidat den” v plánovači.',
'Tip: pokud chcete posunout jen část itineráře, přidejte v plánovači v „Změnit pořadí dnů“ den a přesuňte ho tam, kde má vzniknout mezera.',
'dashboard.shrinkTitle': 'Odebrat dny?',
'dashboard.shrinkIntro': 'Uložením nových dat se odeberou tyto dny:',
'dashboard.shrinkMoreDays': '+{count} další',
'dashboard.shrinkLastDays': 'Odeberou se poslední dny, ne první',
'dashboard.shrinkLastDaysHint':
'Plány dnů se posouvají s novými daty, takže se vždy odeberou poslední dny plánu, i když se posunul začátek.',
'dashboard.shrinkBookingsHint':
'Zůstanou v Rezervacích. Rezervace, jejíž datum je stále součástí cesty, se vrátí na tento den.',
'dashboard.shrinkBookingsShiftHint': 'Zůstanou v Rezervacích, bez dne.',
'dashboard.shrinkStayHint': 'Příjezd nebo odjezd připadá na odebíraný den, proto se odebere celé ubytování.',
'dashboard.shrinkStayBookedHint':
'Příjezd nebo odjezd připadá na odebíraný den, proto se odebere celé ubytování. Jeho rezervace „{booking}“ a její výdaj zůstanou v Rezervacích.',
'dashboard.shrinkConfirm': 'Odebrat dny a uložit',
'dashboard.shrinkUnknown':
'Dny této cesty se nepodařilo ověřit. Pokud mají nová data méně dnů, uložení odebere poslední dny a vše, co je na nich naplánováno.',
'dashboard.greeting.morning': 'Dobré ráno,',
'dashboard.greeting.afternoon': 'Dobré odpoledne,',
'dashboard.greeting.evening': 'Dobrý večer,',
+17 -1
View File
@@ -99,7 +99,23 @@ const dashboard: TranslationStrings = {
'dashboard.dateShiftAllDesc':
'Der gesamte Reiseplan wandert mit den neuen Daten, einschließlich Reservierungen und Unterkünften.',
'dashboard.dateShiftHint':
'Tipp: Um nur einen Teil deines Reiseplans zu verschieben, nutze stattdessen "Tag hinzufügen" im Planer.',
'Tipp: Um nur einen Teil deines Reiseplans zu verschieben, füge im Planer unter „Tage neu anordnen“ einen Tag hinzu und schiebe ihn an die Stelle, an der die Lücke entstehen soll.',
'dashboard.shrinkTitle': 'Tage entfernen?',
'dashboard.shrinkIntro': 'Mit den neuen Daten entfallen beim Speichern diese Tage:',
'dashboard.shrinkMoreDays': '+{count} weitere',
'dashboard.shrinkLastDays': 'Es fallen die letzten Tage weg, nicht die ersten',
'dashboard.shrinkLastDaysHint':
'Die Tagespläne wandern mit den neuen Daten, deshalb entfallen immer die letzten Tage deines Plans, auch wenn sich der Start verschoben hat.',
'dashboard.shrinkBookingsHint':
'Sie bleiben unter Buchungen. Liegt ihr Datum noch in der Reise, kommen sie wieder auf diesen Tag.',
'dashboard.shrinkBookingsShiftHint': 'Sie bleiben unter Buchungen, ohne Tag.',
'dashboard.shrinkStayHint':
'Check-in oder Check-out an einem entfallenden Tag, deshalb wird die ganze Unterkunft entfernt.',
'dashboard.shrinkStayBookedHint':
'Check-in oder Check-out an einem entfallenden Tag, deshalb wird die ganze Unterkunft entfernt. Ihre Buchung „{booking}“ und deren Ausgabe bleiben unter Buchungen.',
'dashboard.shrinkConfirm': 'Tage entfernen und speichern',
'dashboard.shrinkUnknown':
'Die Tage dieser Reise ließen sich nicht prüfen. Haben die neuen Daten weniger Tage, entfernt das Speichern die letzten Tage mit allem, was darauf geplant ist.',
'dashboard.greeting.morning': 'Guten Morgen,',
'dashboard.greeting.afternoon': 'Guten Tag,',
'dashboard.greeting.evening': 'Guten Abend,',
+17 -1
View File
@@ -110,7 +110,23 @@ const dashboard: TranslationStrings = {
'dashboard.dateShiftAll': 'Shift everything',
'dashboard.dateShiftAllDesc':
'The entire itinerary moves with the new dates, including reservations and accommodations.',
'dashboard.dateShiftHint': 'Tip: to shift only part of your itinerary, use "Add day" in the planner instead.',
'dashboard.dateShiftHint':
'Tip: to shift only part of your itinerary, add a day under "Reorder days" in the planner and move it to where the gap belongs.',
'dashboard.shrinkTitle': 'Remove days?',
'dashboard.shrinkIntro': 'Saving the new dates removes these days:',
'dashboard.shrinkMoreDays': '+{count} more',
'dashboard.shrinkLastDays': 'The last days go, not the first',
'dashboard.shrinkLastDaysHint':
'Day plans move with the new dates, so it is always the last days of your plan that go, also when the start moved.',
'dashboard.shrinkBookingsHint':
'They stay under Bookings. One whose date is still part of the trip goes back onto that day.',
'dashboard.shrinkBookingsShiftHint': 'They stay under Bookings, without a day.',
'dashboard.shrinkStayHint': 'Checks in or out on a removed day, so the whole stay is removed.',
'dashboard.shrinkStayBookedHint':
'Checks in or out on a removed day, so the whole stay is removed. Its booking "{booking}" and its expense stay under Bookings.',
'dashboard.shrinkConfirm': 'Remove days and save',
'dashboard.shrinkUnknown':
'The days of this trip could not be checked. If the new dates hold fewer days, saving removes the last days and everything planned on them.',
'dashboard.greeting.morning': 'Good morning,',
'dashboard.greeting.afternoon': 'Good afternoon,',
'dashboard.greeting.evening': 'Good evening,',
+17 -1
View File
@@ -97,7 +97,23 @@ const dashboard: TranslationStrings = {
'dashboard.dateShiftAll': 'Mover todo',
'dashboard.dateShiftAllDesc':
'Todo el itinerario se mueve con las nuevas fechas, incluidas las reservas y los alojamientos.',
'dashboard.dateShiftHint': 'Consejo: para mover solo una parte del itinerario, usa "Añadir día" en el planificador.',
'dashboard.dateShiftHint':
'Consejo: para mover solo una parte del itinerario, añade un día en «Reordenar días» del planificador y muévelo al lugar donde debe quedar el hueco.',
'dashboard.shrinkTitle': '¿Quitar días?',
'dashboard.shrinkIntro': 'Al guardar las nuevas fechas se quitan estos días:',
'dashboard.shrinkMoreDays': '+{count} más',
'dashboard.shrinkLastDays': 'Se quitan los últimos días, no los primeros',
'dashboard.shrinkLastDaysHint':
'Los planes de cada día se mueven con las nuevas fechas, así que siempre se quitan los últimos días del plan, también cuando cambia el inicio.',
'dashboard.shrinkBookingsHint':
'Se quedan en Reservas. Las que tienen una fecha que sigue dentro del viaje vuelven a ese día.',
'dashboard.shrinkBookingsShiftHint': 'Se quedan en Reservas, sin día.',
'dashboard.shrinkStayHint': 'La entrada o la salida es un día que se quita, así que se quita la estancia entera.',
'dashboard.shrinkStayBookedHint':
'La entrada o la salida es un día que se quita, así que se quita la estancia entera. Su reserva «{booking}» y su gasto se quedan en Reservas.',
'dashboard.shrinkConfirm': 'Quitar días y guardar',
'dashboard.shrinkUnknown':
'No se pudieron comprobar los días de este viaje. Si las nuevas fechas tienen menos días, al guardar se quitan los últimos días y todo lo planificado en ellos.',
'dashboard.greeting.morning': 'Buenos días,',
'dashboard.greeting.afternoon': 'Buenas tardes,',
'dashboard.greeting.evening': 'Buenas noches,',
+16 -1
View File
@@ -100,7 +100,22 @@ const dashboard: TranslationStrings = {
'dashboard.dateShiftAllDesc':
"L'itinéraire entier suit les nouvelles dates, y compris les réservations et hébergements.",
'dashboard.dateShiftHint':
"Astuce : pour ne décaler qu'une partie de votre itinéraire, utilisez plutôt « Ajouter un jour » dans le planificateur.",
"Astuce : pour ne décaler qu'une partie de votre itinéraire, ajoutez un jour dans « Réorganiser les jours » du planificateur et placez-le là où l'écart doit se créer.",
'dashboard.shrinkTitle': 'Retirer des jours ?',
'dashboard.shrinkIntro': 'En enregistrant les nouvelles dates, ces jours sont retirés :',
'dashboard.shrinkMoreDays': '+{count} de plus',
'dashboard.shrinkLastDays': 'Ce sont les derniers jours qui partent, pas les premiers',
'dashboard.shrinkLastDaysHint':
'Les programmes des jours suivent les nouvelles dates : ce sont donc toujours les derniers jours de votre plan qui sont retirés, même quand le début a changé.',
'dashboard.shrinkBookingsHint':
'Elles restent dans Réservations. Celles dont la date fait encore partie du voyage reviennent sur ce jour.',
'dashboard.shrinkBookingsShiftHint': 'Elles restent dans Réservations, sans jour.',
'dashboard.shrinkStayHint': "L'arrivée ou le départ tombe un jour retiré, le séjour entier est donc retiré.",
'dashboard.shrinkStayBookedHint':
"L'arrivée ou le départ tombe un jour retiré, le séjour entier est donc retiré. Sa réservation « {booking} » et sa dépense restent dans Réservations.",
'dashboard.shrinkConfirm': 'Retirer les jours et enregistrer',
'dashboard.shrinkUnknown':
"Les jours de ce voyage n'ont pas pu être vérifiés. Si les nouvelles dates comptent moins de jours, l'enregistrement retire les derniers jours et tout ce qui y est prévu.",
'dashboard.greeting.morning': 'Bonjour,',
'dashboard.greeting.afternoon': 'Bon après-midi,',
'dashboard.greeting.evening': 'Bonsoir,',
+17 -1
View File
@@ -110,7 +110,23 @@ const dashboard: TranslationStrings = {
'dashboard.dateShiftAllDesc':
'Ολόκληρο το δρομολόγιο μετακινείται με τις νέες ημερομηνίες, συμπεριλαμβανομένων των κρατήσεων και των καταλυμάτων.',
'dashboard.dateShiftHint':
'Συμβουλή: για να μετατοπίσετε μόνο ένα μέρος του δρομολογίου σας, χρησιμοποιήστε την επιλογή "Προσθήκη ημέρας" στο ημερήσιο πλάνο.',
'Συμβουλή: για να μετατοπίσετε μόνο ένα μέρος του δρομολογίου σας, προσθέστε μια ημέρα από την «Αναδιάταξη ημερών» στο ημερήσιο πλάνο και μετακινήστε την εκεί όπου πρέπει να μείνει το κενό.',
'dashboard.shrinkTitle': 'Αφαίρεση ημερών;',
'dashboard.shrinkIntro': 'Με την αποθήκευση των νέων ημερομηνιών αφαιρούνται αυτές οι ημέρες:',
'dashboard.shrinkMoreDays': '+{count} ακόμη',
'dashboard.shrinkLastDays': 'Αφαιρούνται οι τελευταίες ημέρες, όχι οι πρώτες',
'dashboard.shrinkLastDaysHint':
'Τα πλάνα των ημερών μετακινούνται μαζί με τις νέες ημερομηνίες, οπότε αφαιρούνται πάντα οι τελευταίες ημέρες του πλάνου σας, ακόμη κι όταν άλλαξε η αρχή.',
'dashboard.shrinkBookingsHint':
'Παραμένουν στις Κρατήσεις. Όσες έχουν ημερομηνία που ανήκει ακόμη στο ταξίδι επιστρέφουν σε εκείνη την ημέρα.',
'dashboard.shrinkBookingsShiftHint': 'Παραμένουν στις Κρατήσεις, χωρίς ημέρα.',
'dashboard.shrinkStayHint':
'Η άφιξη ή η αναχώρηση πέφτει σε ημέρα που αφαιρείται, οπότε αφαιρείται ολόκληρη η διαμονή.',
'dashboard.shrinkStayBookedHint':
'Η άφιξη ή η αναχώρηση πέφτει σε ημέρα που αφαιρείται, οπότε αφαιρείται ολόκληρη η διαμονή. Η κράτησή της «{booking}» και το έξοδό της παραμένουν στις Κρατήσεις.',
'dashboard.shrinkConfirm': 'Αφαίρεση ημερών και αποθήκευση',
'dashboard.shrinkUnknown':
'Δεν ήταν δυνατός ο έλεγχος των ημερών αυτού του ταξιδιού. Αν οι νέες ημερομηνίες έχουν λιγότερες ημέρες, η αποθήκευση αφαιρεί τις τελευταίες ημέρες και ό,τι έχει προγραμματιστεί σε αυτές.',
'dashboard.greeting.morning': 'Καλημέρα,',
'dashboard.greeting.afternoon': 'Καλό απόγευμα,',
'dashboard.greeting.evening': 'Καλησπέρα,',
+17 -1
View File
@@ -99,7 +99,23 @@ const dashboard: TranslationStrings = {
'dashboard.dateShiftAllDesc':
'A teljes útiterv az új dátumokkal együtt mozog, beleértve a foglalásokat és a szállásokat is.',
'dashboard.dateShiftHint':
'Tipp: ha csak az útiterv egy részét szeretnéd eltolni, használd inkább a "Nap hozzáadása" funkciót a tervezőben.',
'Tipp: ha csak az útiterv egy részét szeretnéd eltolni, a tervezőben a „Napok átrendezése” alatt adj hozzá egy napot, és vidd oda, ahol a résnek lennie kell.',
'dashboard.shrinkTitle': 'Eltávolítod a napokat?',
'dashboard.shrinkIntro': 'Az új dátumok mentésekor ezek a napok eltávolításra kerülnek:',
'dashboard.shrinkMoreDays': '+{count} további',
'dashboard.shrinkLastDays': 'Az utolsó napok kerülnek ki, nem az elsők',
'dashboard.shrinkLastDaysHint':
'A napi tervek az új dátumokkal együtt mozognak, ezért mindig a terv utolsó napjai kerülnek ki, akkor is, ha a kezdés változott.',
'dashboard.shrinkBookingsHint':
'A Foglalások között maradnak. Amelyiknek a dátuma még az utazásba esik, visszakerül arra a napra.',
'dashboard.shrinkBookingsShiftHint': 'A Foglalások között maradnak, nap nélkül.',
'dashboard.shrinkStayHint':
'A bejelentkezés vagy a kijelentkezés egy eltávolított napra esik, ezért az egész szállás eltávolításra kerül.',
'dashboard.shrinkStayBookedHint':
'A bejelentkezés vagy a kijelentkezés egy eltávolított napra esik, ezért az egész szállás eltávolításra kerül. A(z) „{booking}” foglalás és annak kiadása a Foglalások között marad.',
'dashboard.shrinkConfirm': 'Napok eltávolítása és mentés',
'dashboard.shrinkUnknown':
'Az utazás napjait nem sikerült ellenőrizni. Ha az új dátumok kevesebb napot fognak át, a mentés eltávolítja az utolsó napokat és mindent, ami rájuk van tervezve.',
'dashboard.greeting.morning': 'Jó reggelt,',
'dashboard.greeting.afternoon': 'Jó napot,',
'dashboard.greeting.evening': 'Jó estét,',
+16 -1
View File
@@ -98,7 +98,22 @@ const dashboard: TranslationStrings = {
'dashboard.dateShiftAllDesc':
'Seluruh rencana perjalanan berpindah ke tanggal baru, termasuk reservasi dan akomodasi.',
'dashboard.dateShiftHint':
'Tip: untuk menggeser sebagian rencana perjalanan saja, gunakan "Tambah hari" di perencana.',
'Tip: untuk menggeser sebagian rencana perjalanan saja, tambahkan satu hari di “Atur ulang hari” pada perencana lalu pindahkan ke tempat jeda seharusnya berada.',
'dashboard.shrinkTitle': 'Hapus hari?',
'dashboard.shrinkIntro': 'Menyimpan tanggal baru akan menghapus hari-hari ini:',
'dashboard.shrinkMoreDays': '+{count} lainnya',
'dashboard.shrinkLastDays': 'Yang terhapus hari-hari terakhir, bukan yang pertama',
'dashboard.shrinkLastDaysHint':
'Rencana harian ikut bergeser bersama tanggal baru, jadi yang terhapus selalu hari-hari terakhir rencana Anda, juga saat tanggal mulai berubah.',
'dashboard.shrinkBookingsHint':
'Tetap ada di Pemesanan. Pemesanan yang tanggalnya masih termasuk dalam perjalanan kembali ke hari tersebut.',
'dashboard.shrinkBookingsShiftHint': 'Tetap ada di Pemesanan, tanpa hari.',
'dashboard.shrinkStayHint': 'Check-in atau check-out jatuh pada hari yang dihapus, jadi seluruh penginapan dihapus.',
'dashboard.shrinkStayBookedHint':
'Check-in atau check-out jatuh pada hari yang dihapus, jadi seluruh penginapan dihapus. Pemesanan “{booking}” dan pengeluarannya tetap ada di Pemesanan.',
'dashboard.shrinkConfirm': 'Hapus hari dan simpan',
'dashboard.shrinkUnknown':
'Hari-hari perjalanan ini tidak dapat diperiksa. Jika tanggal baru mencakup lebih sedikit hari, menyimpan akan menghapus hari-hari terakhir beserta semua yang direncanakan di dalamnya.',
'dashboard.greeting.morning': 'Selamat pagi,',
'dashboard.greeting.afternoon': 'Selamat siang,',
'dashboard.greeting.evening': 'Selamat malam,',
+17 -1
View File
@@ -99,7 +99,23 @@ const dashboard: TranslationStrings = {
'dashboard.dateShiftAll': 'Sposta tutto',
'dashboard.dateShiftAllDesc': "L'intero itinerario si sposta con le nuove date, incluse prenotazioni e alloggi.",
'dashboard.dateShiftHint':
'Suggerimento: per spostare solo una parte dell\'itinerario, usa "Aggiungi giorno" nel planner.',
"Suggerimento: per spostare solo una parte dell'itinerario, aggiungi un giorno in «Riordina i giorni» nel planner e spostalo dove deve crearsi lo spazio.",
'dashboard.shrinkTitle': 'Rimuovere dei giorni?',
'dashboard.shrinkIntro': 'Salvando le nuove date vengono rimossi questi giorni:',
'dashboard.shrinkMoreDays': '+{count} altri',
'dashboard.shrinkLastDays': 'Vengono rimossi gli ultimi giorni, non i primi',
'dashboard.shrinkLastDaysHint':
"I programmi dei giorni seguono le nuove date, quindi vengono sempre rimossi gli ultimi giorni del piano, anche quando è cambiato l'inizio.",
'dashboard.shrinkBookingsHint':
'Restano in Prenotazioni. Quelle la cui data fa ancora parte del viaggio tornano su quel giorno.',
'dashboard.shrinkBookingsShiftHint': 'Restano in Prenotazioni, senza giorno.',
'dashboard.shrinkStayHint':
"Il check-in o il check-out cade in un giorno rimosso, quindi viene rimosso l'intero soggiorno.",
'dashboard.shrinkStayBookedHint':
"Il check-in o il check-out cade in un giorno rimosso, quindi viene rimosso l'intero soggiorno. La sua prenotazione «{booking}» e la relativa spesa restano in Prenotazioni.",
'dashboard.shrinkConfirm': 'Rimuovi i giorni e salva',
'dashboard.shrinkUnknown':
'Non è stato possibile controllare i giorni di questo viaggio. Se le nuove date hanno meno giorni, salvando vengono rimossi gli ultimi giorni e tutto ciò che vi è pianificato.',
'dashboard.greeting.morning': 'Buongiorno,',
'dashboard.greeting.afternoon': 'Buon pomeriggio,',
'dashboard.greeting.evening': 'Buonasera,',
+16 -1
View File
@@ -110,7 +110,22 @@ const dashboard: TranslationStrings = {
'日別計画は新しい日程に合わせて移動しますが、予約と宿泊先は旅行期間内にある限り元の日付のままになります。',
'dashboard.dateShiftAll': 'すべてを移動する',
'dashboard.dateShiftAllDesc': '予約や宿泊先を含む旅程全体が新しい日程に合わせて移動します。',
'dashboard.dateShiftHint': 'ヒント:旅程の一部だけを移動したい場合は、プランナーの「日付を追加」を使用してください。',
'dashboard.dateShiftHint':
'ヒント:旅程の一部だけをずらしたい場合は、プランナーの「日付を並べ替え」で日を追加し、空けたい位置へ移動してください。',
'dashboard.shrinkTitle': '日を削除しますか?',
'dashboard.shrinkIntro': '新しい日付を保存すると、次の日が削除されます:',
'dashboard.shrinkMoreDays': 'ほか{count}日',
'dashboard.shrinkLastDays': '削除されるのは最初ではなく最後の日です',
'dashboard.shrinkLastDaysHint':
'各日のプランは新しい日付と一緒に移動するため、開始日を変えた場合でも、削除されるのは常にプランの最後の日です。',
'dashboard.shrinkBookingsHint': '予約に残ります。日付がまだ旅行の期間内にある予約は、その日に戻ります。',
'dashboard.shrinkBookingsShiftHint': '日付なしで予約に残ります。',
'dashboard.shrinkStayHint': '削除される日にチェックインまたはチェックアウトするため、宿泊全体が削除されます。',
'dashboard.shrinkStayBookedHint':
'削除される日にチェックインまたはチェックアウトするため、宿泊全体が削除されます。予約「{booking}」とその支出は予約に残ります。',
'dashboard.shrinkConfirm': '日を削除して保存',
'dashboard.shrinkUnknown':
'この旅行の日を確認できませんでした。新しい日付の日数が少ない場合、保存すると最後の日とそこに予定されているものがすべて削除されます。',
'dashboard.greeting.morning': 'おはようございます、',
'dashboard.greeting.afternoon': 'こんにちは、',
'dashboard.greeting.evening': 'こんばんは、',
+16 -1
View File
@@ -110,7 +110,22 @@ const dashboard: TranslationStrings = {
'일별 계획은 새 날짜로 이동하고, 예약과 숙박은 여행 기간에 포함되어 있는 한 원래 날짜에 그대로 유지됩니다.',
'dashboard.dateShiftAll': '모두 이동',
'dashboard.dateShiftAllDesc': '예약과 숙박을 포함한 전체 일정이 새 날짜로 이동합니다.',
'dashboard.dateShiftHint': '팁: 일정의 일부만 이동하려면 플래너의 "날짜 추가" 기능을 사용하세요.',
'dashboard.dateShiftHint':
'팁: 일정의 일부만 옮기려면 플래너의 "날짜 순서 변경"에서 날짜를 추가한 뒤 빈칸이 생겨야 할 위치로 옮기세요.',
'dashboard.shrinkTitle': '날짜를 삭제할까요?',
'dashboard.shrinkIntro': '새 날짜를 저장하면 다음 날짜가 삭제됩니다:',
'dashboard.shrinkMoreDays': '외 {count}일',
'dashboard.shrinkLastDays': '처음이 아니라 마지막 날짜가 삭제됩니다',
'dashboard.shrinkLastDaysHint':
'각 날짜의 계획은 새 날짜와 함께 이동하므로, 시작일이 바뀌어도 항상 계획의 마지막 날짜가 삭제됩니다.',
'dashboard.shrinkBookingsHint': '예약에 남습니다. 날짜가 아직 여행 기간에 포함된 예약은 해당 날짜로 돌아갑니다.',
'dashboard.shrinkBookingsShiftHint': '날짜 없이 예약에 남습니다.',
'dashboard.shrinkStayHint': '삭제되는 날짜에 체크인 또는 체크아웃하므로 숙박 전체가 삭제됩니다.',
'dashboard.shrinkStayBookedHint':
'삭제되는 날짜에 체크인 또는 체크아웃하므로 숙박 전체가 삭제됩니다. 예약 “{booking}”과 해당 지출은 예약에 남습니다.',
'dashboard.shrinkConfirm': '날짜 삭제 후 저장',
'dashboard.shrinkUnknown':
'이 여행의 날짜를 확인할 수 없습니다. 새 날짜의 일수가 더 적으면 저장 시 마지막 날짜와 그날 계획된 모든 항목이 삭제됩니다.',
'dashboard.greeting.morning': '좋은 아침이에요,',
'dashboard.greeting.afternoon': '안녕하세요,',
'dashboard.greeting.evening': '좋은 저녁이에요,',
+17 -1
View File
@@ -98,7 +98,23 @@ const dashboard: TranslationStrings = {
'dashboard.dateShiftAllDesc':
'De volledige reisplanning verschuift mee met de nieuwe datums, inclusief reserveringen en accommodaties.',
'dashboard.dateShiftHint':
'Tip: om slechts een deel van je reisplanning te verschuiven, gebruik je "Dag toevoegen" in de planner.',
'Tip: om maar een deel van je reisplanning te verschuiven, voeg je in de planner onder ‘Dagen herordenen’ een dag toe en zet je die op de plek waar het gat moet komen.',
'dashboard.shrinkTitle': 'Dagen verwijderen?',
'dashboard.shrinkIntro': 'Bij het opslaan van de nieuwe datums worden deze dagen verwijderd:',
'dashboard.shrinkMoreDays': '+{count} meer',
'dashboard.shrinkLastDays': 'De laatste dagen vallen weg, niet de eerste',
'dashboard.shrinkLastDaysHint':
'Dagplanningen schuiven mee met de nieuwe datums, dus het zijn altijd de laatste dagen van je planning die wegvallen, ook als het begin is verschoven.',
'dashboard.shrinkBookingsHint':
'Ze blijven onder Boekingen. Een boeking waarvan de datum nog binnen de reis valt, komt terug op die dag.',
'dashboard.shrinkBookingsShiftHint': 'Ze blijven onder Boekingen, zonder dag.',
'dashboard.shrinkStayHint':
'Inchecken of uitchecken valt op een dag die wegvalt, dus het hele verblijf wordt verwijderd.',
'dashboard.shrinkStayBookedHint':
'Inchecken of uitchecken valt op een dag die wegvalt, dus het hele verblijf wordt verwijderd. De boeking ‘{booking}’ en de bijbehorende uitgave blijven onder Boekingen.',
'dashboard.shrinkConfirm': 'Dagen verwijderen en opslaan',
'dashboard.shrinkUnknown':
'De dagen van deze reis konden niet worden gecontroleerd. Hebben de nieuwe datums minder dagen, dan verwijdert opslaan de laatste dagen en alles wat daarop gepland is.',
'dashboard.greeting.morning': 'Goedemorgen,',
'dashboard.greeting.afternoon': 'Goedemiddag,',
'dashboard.greeting.evening': 'Goedenavond,',
+17 -1
View File
@@ -92,7 +92,23 @@ const dashboard: TranslationStrings = {
'Plany dnia przesuną się wraz z nowymi datami, a rezerwacje i noclegi pozostaną w swoich pierwotnych terminach, o ile nadal mieszczą się one w ramach podróży.',
'dashboard.dateShiftAll': 'Przesuń wszystko',
'dashboard.dateShiftAllDesc': 'Cały plan podróży przesunie się wraz z nowymi datami, w tym rezerwacje i noclegi.',
'dashboard.dateShiftHint': 'Wskazówka: aby przesunąć tylko część planu podróży, użyj opcji "Dodaj dzień" w planerze.',
'dashboard.dateShiftHint':
'Wskazówka: aby przesunąć tylko część planu podróży, dodaj dzień w „Zmień kolejność dni” w planerze i przenieś go tam, gdzie ma powstać przerwa.',
'dashboard.shrinkTitle': 'Usunąć dni?',
'dashboard.shrinkIntro': 'Zapisanie nowych dat usunie te dni:',
'dashboard.shrinkMoreDays': '+{count} więcej',
'dashboard.shrinkLastDays': 'Usuwane są ostatnie dni, nie pierwsze',
'dashboard.shrinkLastDaysHint':
'Plany dni przesuwają się razem z nowymi datami, więc zawsze usuwane są ostatnie dni planu, także gdy zmienił się początek.',
'dashboard.shrinkBookingsHint':
'Pozostają w Rezerwacjach. Rezerwacja, której data nadal mieści się w podróży, wraca na ten dzień.',
'dashboard.shrinkBookingsShiftHint': 'Pozostają w Rezerwacjach, bez dnia.',
'dashboard.shrinkStayHint': 'Zameldowanie lub wymeldowanie przypada na usuwany dzień, więc usuwany jest cały nocleg.',
'dashboard.shrinkStayBookedHint':
'Zameldowanie lub wymeldowanie przypada na usuwany dzień, więc usuwany jest cały nocleg. Jego rezerwacja „{booking}” i jej wydatek pozostają w Rezerwacjach.',
'dashboard.shrinkConfirm': 'Usuń dni i zapisz',
'dashboard.shrinkUnknown':
'Nie udało się sprawdzić dni tej podróży. Jeśli nowe daty obejmują mniej dni, zapisanie usunie ostatnie dni i wszystko, co na nie zaplanowano.',
'dashboard.members': 'Współpodróżnicy',
'dashboard.copyTrip': 'Kopiuj',
'dashboard.copySuffix': 'kopia',
+16 -1
View File
@@ -96,7 +96,22 @@ const dashboard: TranslationStrings = {
'dashboard.dateShiftAll': 'Перенести всё',
'dashboard.dateShiftAllDesc': 'Весь маршрут переносится на новые даты, включая бронирования и жильё.',
'dashboard.dateShiftHint':
'Совет: чтобы перенести только часть маршрута, используйте «Добавить день» в планировщике.',
'Совет: чтобы сдвинуть только часть маршрута, добавьте день в разделе «Изменить порядок дней» в планировщике и переместите его туда, где должен появиться промежуток.',
'dashboard.shrinkTitle': 'Удалить дни?',
'dashboard.shrinkIntro': 'При сохранении новых дат будут удалены эти дни:',
'dashboard.shrinkMoreDays': 'ещё +{count}',
'dashboard.shrinkLastDays': 'Удаляются последние дни, а не первые',
'dashboard.shrinkLastDaysHint':
'Планы дней переносятся вместе с новыми датами, поэтому удаляются всегда последние дни плана, даже если сдвинулось начало.',
'dashboard.shrinkBookingsHint':
'Они останутся в Бронированиях. Бронирование, дата которого ещё входит в поездку, вернётся на этот день.',
'dashboard.shrinkBookingsShiftHint': 'Они останутся в Бронированиях, без дня.',
'dashboard.shrinkStayHint': 'Заезд или выезд приходится на удаляемый день, поэтому проживание удаляется целиком.',
'dashboard.shrinkStayBookedHint':
'Заезд или выезд приходится на удаляемый день, поэтому проживание удаляется целиком. Его бронирование «{booking}» и расход по нему останутся в Бронированиях.',
'dashboard.shrinkConfirm': 'Удалить дни и сохранить',
'dashboard.shrinkUnknown':
'Не удалось проверить дни этой поездки. Если в новых датах меньше дней, при сохранении будут удалены последние дни и всё, что на них запланировано.',
'dashboard.greeting.morning': 'Доброе утро,',
'dashboard.greeting.afternoon': 'Добрый день,',
'dashboard.greeting.evening': 'Добрый вечер,',
+16 -1
View File
@@ -110,7 +110,22 @@ const dashboard: TranslationStrings = {
'dashboard.dateShiftAll': 'Flytta allt',
'dashboard.dateShiftAllDesc': 'Hela resplanen flyttas med de nya datumen, inklusive bokningar och boenden.',
'dashboard.dateShiftHint':
'Tips: om du bara vill flytta en del av resplanen kan du använda "Lägg till dag" i planeraren istället.',
'Tips: om du bara vill flytta en del av resplanen kan du lägga till en dag under ”Sortera om dagar” i planeraren och flytta den dit luckan ska vara.',
'dashboard.shrinkTitle': 'Ta bort dagar?',
'dashboard.shrinkIntro': 'När de nya datumen sparas tas de här dagarna bort:',
'dashboard.shrinkMoreDays': '+{count} till',
'dashboard.shrinkLastDays': 'Det är de sista dagarna som försvinner, inte de första',
'dashboard.shrinkLastDaysHint':
'Dagsplanerna följer med de nya datumen, så det är alltid de sista dagarna i planen som tas bort, även när starten har flyttats.',
'dashboard.shrinkBookingsHint':
'De ligger kvar under Bokningar. En bokning vars datum fortfarande ingår i resan hamnar på den dagen igen.',
'dashboard.shrinkBookingsShiftHint': 'De ligger kvar under Bokningar, utan dag.',
'dashboard.shrinkStayHint': 'Incheckning eller utcheckning sker en dag som tas bort, så hela boendet tas bort.',
'dashboard.shrinkStayBookedHint':
'Incheckning eller utcheckning sker en dag som tas bort, så hela boendet tas bort. Bokningen ”{booking}” och dess utgift ligger kvar under Bokningar.',
'dashboard.shrinkConfirm': 'Ta bort dagar och spara',
'dashboard.shrinkUnknown':
'Resans dagar kunde inte kontrolleras. Om de nya datumen rymmer färre dagar tar sparandet bort de sista dagarna och allt som är planerat på dem.',
'dashboard.greeting.morning': 'God morgon,',
'dashboard.greeting.afternoon': 'God eftermiddag,',
'dashboard.greeting.evening': 'God kväll,',
+16 -1
View File
@@ -111,7 +111,22 @@ const dashboard: TranslationStrings = {
'dashboard.dateShiftAll': 'Her şeyi kaydır',
'dashboard.dateShiftAllDesc': 'Rezervasyonlar ve konaklamalar dahil tüm plan yeni tarihlerle birlikte taşınır.',
'dashboard.dateShiftHint':
'İpucu: planın yalnızca bir kısmını kaydırmak için planlayıcıdaki "Gün ekle" özelliğini kullanın.',
'İpucu: planın yalnızca bir kısmını kaydırmak için planlayıcıda “Günleri yeniden sırala” altında bir gün ekleyin ve boşluğun olacağı yere taşıyın.',
'dashboard.shrinkTitle': 'Günler kaldırılsın mı?',
'dashboard.shrinkIntro': 'Yeni tarihler kaydedilince bu günler kaldırılır:',
'dashboard.shrinkMoreDays': '+{count} daha',
'dashboard.shrinkLastDays': 'İlk günler değil, son günler kaldırılır',
'dashboard.shrinkLastDaysHint':
'Gün planları yeni tarihlerle birlikte kayar, bu yüzden başlangıç değişse bile her zaman planın son günleri kaldırılır.',
'dashboard.shrinkBookingsHint':
'Rezervasyonlar altında kalırlar. Tarihi hâlâ seyahatin içinde olan rezervasyon o güne geri döner.',
'dashboard.shrinkBookingsShiftHint': 'Rezervasyonlar altında, bir güne bağlı olmadan kalırlar.',
'dashboard.shrinkStayHint': 'Giriş veya çıkış kaldırılan bir güne denk geldiği için konaklamanın tamamı kaldırılır.',
'dashboard.shrinkStayBookedHint':
'Giriş veya çıkış kaldırılan bir güne denk geldiği için konaklamanın tamamı kaldırılır. “{booking}” rezervasyonu ve harcaması Rezervasyonlar altında kalır.',
'dashboard.shrinkConfirm': 'Günleri kaldır ve kaydet',
'dashboard.shrinkUnknown':
'Bu seyahatin günleri kontrol edilemedi. Yeni tarihler daha az gün içeriyorsa kaydetmek son günleri ve bunlara planlanan her şeyi kaldırır.',
'dashboard.greeting.morning': 'Günaydın,',
'dashboard.greeting.afternoon': 'İyi günler,',
'dashboard.greeting.evening': 'İyi akşamlar,',
+17 -1
View File
@@ -111,7 +111,23 @@ const dashboard: TranslationStrings = {
'Плани днів переміщуються разом із новими датами, а бронювання та проживання залишаються на своїх початкових датах, поки ті все ще входять до поїздки.',
'dashboard.dateShiftAll': 'Перемістити все',
'dashboard.dateShiftAllDesc': 'Весь маршрут переміщується на нові дати, включно з бронюваннями та проживанням.',
'dashboard.dateShiftHint': 'Порада: щоб зсунути лише частину маршруту, скористайтеся «Додати день» у планувальнику.',
'dashboard.dateShiftHint':
'Порада: щоб зсунути лише частину маршруту, додайте день у розділі «Змінити порядок днів» у планувальнику й перемістіть його туди, де має з’явитися проміжок.',
'dashboard.shrinkTitle': 'Видалити дні?',
'dashboard.shrinkIntro': 'Під час збереження нових дат буде видалено ці дні:',
'dashboard.shrinkMoreDays': 'ще +{count}',
'dashboard.shrinkLastDays': 'Видаляються останні дні, а не перші',
'dashboard.shrinkLastDaysHint':
'Плани днів переносяться разом із новими датами, тому завжди видаляються останні дні плану, навіть якщо зсунувся початок.',
'dashboard.shrinkBookingsHint':
'Вони залишаться в Бронюваннях. Бронювання, дата якого ще входить у подорож, повернеться на цей день.',
'dashboard.shrinkBookingsShiftHint': 'Вони залишаться в Бронюваннях, без дня.',
'dashboard.shrinkStayHint': 'Заїзд або виїзд припадає на день, що видаляється, тому проживання видаляється повністю.',
'dashboard.shrinkStayBookedHint':
'Заїзд або виїзд припадає на день, що видаляється, тому проживання видаляється повністю. Його бронювання «{booking}» і витрата за ним залишаться в Бронюваннях.',
'dashboard.shrinkConfirm': 'Видалити дні й зберегти',
'dashboard.shrinkUnknown':
'Не вдалося перевірити дні цієї подорожі. Якщо в нових датах менше днів, під час збереження буде видалено останні дні й усе, що на них заплановано.',
'dashboard.greeting.morning': 'Доброго ранку,',
'dashboard.greeting.afternoon': 'Доброго дня,',
'dashboard.greeting.evening': 'Доброго вечора,',
+17 -1
View File
@@ -104,7 +104,23 @@ const dashboard: TranslationStrings = {
'Kế hoạch theo ngày sẽ dời theo ngày mới, trong khi đặt chỗ và chỗ ở vẫn giữ nguyên ngày ban đầu miễn là những ngày đó vẫn nằm trong chuyến đi.',
'dashboard.dateShiftAll': 'Dời tất cả',
'dashboard.dateShiftAllDesc': 'Toàn bộ lịch trình sẽ dời theo ngày mới, bao gồm cả đặt chỗ và chỗ ở.',
'dashboard.dateShiftHint': 'Mẹo: để chỉ dời một phần lịch trình, hãy dùng "Thêm ngày" trong trình lập kế hoạch.',
'dashboard.dateShiftHint':
'Mẹo: để chỉ dời một phần lịch trình, hãy thêm một ngày trong “Sắp xếp lại ngày” ở trình lập kế hoạch rồi chuyển nó đến chỗ cần tạo khoảng trống.',
'dashboard.shrinkTitle': 'Bỏ bớt ngày?',
'dashboard.shrinkIntro': 'Khi lưu ngày mới, những ngày này sẽ bị bỏ:',
'dashboard.shrinkMoreDays': '+{count} ngày khác',
'dashboard.shrinkLastDays': 'Những ngày cuối bị bỏ, không phải những ngày đầu',
'dashboard.shrinkLastDaysHint':
'Kế hoạch từng ngày dời theo ngày mới, nên luôn là những ngày cuối của kế hoạch bị bỏ, kể cả khi ngày bắt đầu đã thay đổi.',
'dashboard.shrinkBookingsHint':
'Chúng vẫn nằm trong Đặt chỗ. Đặt chỗ nào có ngày vẫn thuộc chuyến đi sẽ quay lại ngày đó.',
'dashboard.shrinkBookingsShiftHint': 'Chúng vẫn nằm trong Đặt chỗ, không gắn với ngày nào.',
'dashboard.shrinkStayHint': 'Nhận phòng hoặc trả phòng rơi vào một ngày bị bỏ, nên toàn bộ chỗ lưu trú bị bỏ.',
'dashboard.shrinkStayBookedHint':
'Nhận phòng hoặc trả phòng rơi vào một ngày bị bỏ, nên toàn bộ chỗ lưu trú bị bỏ. Đặt chỗ “{booking}” và chi phí của nó vẫn nằm trong Đặt chỗ.',
'dashboard.shrinkConfirm': 'Bỏ ngày và lưu',
'dashboard.shrinkUnknown':
'Không thể kiểm tra các ngày của chuyến đi này. Nếu ngày mới có ít ngày hơn, việc lưu sẽ bỏ những ngày cuối và mọi thứ đã lên kế hoạch trong đó.',
'dashboard.greeting.morning': 'Chào buổi sáng,',
'dashboard.greeting.afternoon': 'Chào buổi chiều,',
'dashboard.greeting.evening': 'Buổi tối vui vẻ,',
+15 -1
View File
@@ -95,7 +95,21 @@ const dashboard: TranslationStrings = {
'每日計劃會隨新日期移動,而預訂和住宿只要仍在旅行期間內,就會保留在原本的日期。',
'dashboard.dateShiftAll': '全部移動',
'dashboard.dateShiftAllDesc': '整個行程都會隨新日期移動,包括預訂和住宿。',
'dashboard.dateShiftHint': '提示:若只想移動部分行程,請改用規劃頁面中的「新增日期」。',
'dashboard.dateShiftHint':
'提示:若只想移動部分行程,請在規劃頁面的「重新排序日期」中新增一天,並將它移到需要空出的位置。',
'dashboard.shrinkTitle': '移除天數?',
'dashboard.shrinkIntro': '儲存新日期後,將移除以下這些天:',
'dashboard.shrinkMoreDays': '另外 {count} 天',
'dashboard.shrinkLastDays': '移除的是最後幾天,而不是最前面的幾天',
'dashboard.shrinkLastDaysHint': '每天的計畫會隨新日期一起移動,因此即使開始日期改變,移除的也總是計畫中的最後幾天。',
'dashboard.shrinkBookingsHint': '它們會保留在「預訂」中。日期仍在旅行範圍內的預訂會回到對應的那一天。',
'dashboard.shrinkBookingsShiftHint': '它們會保留在「預訂」中,但不再屬於任何一天。',
'dashboard.shrinkStayHint': '入住或退房在被移除的某一天,因此整個住宿都會被移除。',
'dashboard.shrinkStayBookedHint':
'入住或退房在被移除的某一天,因此整個住宿都會被移除。其預訂「{booking}」及費用會保留在「預訂」中。',
'dashboard.shrinkConfirm': '移除天數並儲存',
'dashboard.shrinkUnknown':
'無法檢查此旅行的天數。如果新日期包含的天數較少,儲存時會移除最後幾天及其中規劃的所有內容。',
'dashboard.greeting.morning': '早安,',
'dashboard.greeting.afternoon': '午安,',
'dashboard.greeting.evening': '晚安,',
+15 -1
View File
@@ -95,7 +95,21 @@ const dashboard: TranslationStrings = {
'日程计划将随新日期移动,而预订和住宿只要仍在旅行日期范围内,就保留其原定日期。',
'dashboard.dateShiftAll': '全部顺移',
'dashboard.dateShiftAllDesc': '整个行程将随新日期移动,包括预订和住宿。',
'dashboard.dateShiftHint': '提示:如需仅移动部分行程,请改用行程规划中的「添加一天」。',
'dashboard.dateShiftHint':
'提示:如需仅移动部分行程,请在行程规划的“调整日期顺序”中添加一天,并将其移到需要留出空档的位置。',
'dashboard.shrinkTitle': '移除天数?',
'dashboard.shrinkIntro': '保存新日期后,将移除以下这些天:',
'dashboard.shrinkMoreDays': '另外 {count} 天',
'dashboard.shrinkLastDays': '移除的是最后几天,而不是最前面的几天',
'dashboard.shrinkLastDaysHint': '每天的计划会随新日期一起移动,因此即使开始日期变了,移除的也总是计划中的最后几天。',
'dashboard.shrinkBookingsHint': '它们会保留在“预订”中。日期仍在旅行范围内的预订会回到对应的那一天。',
'dashboard.shrinkBookingsShiftHint': '它们会保留在“预订”中,但不再属于任何一天。',
'dashboard.shrinkStayHint': '入住或退房在被移除的某一天,因此整个住宿都会被移除。',
'dashboard.shrinkStayBookedHint':
'入住或退房在被移除的某一天,因此整个住宿都会被移除。其预订“{booking}”及费用会保留在“预订”中。',
'dashboard.shrinkConfirm': '移除天数并保存',
'dashboard.shrinkUnknown':
'无法检查此旅行的天数。如果新日期包含的天数更少,保存时会移除最后几天及其中计划的所有内容。',
'dashboard.greeting.morning': '早上好,',
'dashboard.greeting.afternoon': '下午好,',
'dashboard.greeting.evening': '晚上好,',
+171 -1
View File
@@ -1,4 +1,12 @@
import { addIsoDays, nextTripDate, planDatedAppend } from './day-grid';
import {
type DayGridDay,
type DayGridStay,
addIsoDays,
nextTripDate,
planDatedAppend,
planDayGrid,
resolveDayGridRange,
} from './day-grid';
import { MAX_TRIP_DAYS } from './trip.schema';
import { describe, expect, it } from 'vitest';
@@ -68,3 +76,165 @@ describe('planDatedAppend', () => {
expect(planDatedAppend({}, ['2026-10-10'])).toBeNull();
});
});
describe('resolveDayGridRange', () => {
const dated = { start_date: '2026-10-01', end_date: '2026-10-10' };
it('keeps a stored date the update leaves out, and clears one it sends as null', () => {
expect(resolveDayGridRange(dated, { end_date: '2026-10-08' })).toEqual({
newStart: '2026-10-01',
newEnd: '2026-10-08',
dayCount: undefined,
regenerate: true,
});
expect(resolveDayGridRange(dated, { start_date: null, end_date: null })).toMatchObject({
newStart: null,
newEnd: null,
regenerate: true,
});
});
it('rebuilds nothing when the dates stay and no day count comes', () => {
expect(resolveDayGridRange(dated, {}).regenerate).toBe(false);
expect(resolveDayGridRange(dated, { ...dated }).regenerate).toBe(false);
const undated = { start_date: null, end_date: null };
expect(resolveDayGridRange(undated, { ...undated }).regenerate).toBe(false);
});
it('rebuilds on any day count, held to 1..MAX_TRIP_DAYS, and reads 0 or null as none', () => {
const undated = { start_date: null, end_date: null };
expect(resolveDayGridRange(undated, { day_count: 5 })).toMatchObject({ dayCount: 5, regenerate: true });
expect(resolveDayGridRange(undated, { day_count: MAX_TRIP_DAYS + 5 }).dayCount).toBe(MAX_TRIP_DAYS);
expect(resolveDayGridRange(undated, { day_count: -3 }).dayCount).toBe(1);
expect(resolveDayGridRange(undated, { day_count: 0 })).toMatchObject({ dayCount: undefined, regenerate: false });
expect(resolveDayGridRange(undated, { day_count: null })).toMatchObject({ dayCount: undefined, regenerate: false });
});
});
describe('planDayGrid', () => {
// The trip of the plan: Oct 1 to 10 with a day each (ids 1 to 10), then two
// days without a date: 11 is empty, 12 has a place on it.
const tenDays: DayGridDay[] = [
...Array.from({ length: 10 }, (_, i) => ({
id: i + 1,
day_number: i + 1,
date: addIsoDays('2026-10-01', i),
hasPlanItems: true,
})),
{ id: 11, day_number: 11, date: null, hasPlanItems: false },
{ id: 12, day_number: 12, date: null, hasPlanItems: true },
];
const plan = (startDate: string | null, endDate: string | null, stays: DayGridStay[] = []) =>
planDayGrid({ days: tenDays, stays, startDate, endDate });
const ids = (p: ReturnType<typeof plan>) => p.rows.map((r) => r.id);
const gone = (p: ReturnType<typeof plan>) => p.removed.map((r) => `${r.id}:${r.reason}`);
it('an earlier end takes the last days with everything on them, and the empty spare day', () => {
const p = plan('2026-10-01', '2026-10-08');
expect(ids(p)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 12]);
expect(p.rows.at(-2)).toEqual({ id: 8, date: '2026-10-08' });
expect(p.rows.at(-1)).toEqual({ id: 12, date: null });
expect(gone(p)).toEqual(['9:overflow', '10:overflow', '11:spare']);
expect(p.removed[0]).toEqual({ id: 9, day_number: 9, date: '2026-10-09', reason: 'overflow' });
});
it('a later start and the same end still takes the last days, the plans move with the dates', () => {
const p = plan('2026-10-03', '2026-10-10');
expect(p.rows.slice(0, 8)).toEqual(
Array.from({ length: 8 }, (_, i) => ({ id: i + 1, date: addIsoDays('2026-10-03', i) })),
);
expect(gone(p)).toEqual(['9:overflow', '10:overflow', '11:spare']);
});
it('moving the whole range keeps every dated day and drops only the empty spare day', () => {
const p = plan('2026-10-05', '2026-10-14');
expect(ids(p)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12]);
expect(p.rows[9]).toEqual({ id: 10, date: '2026-10-14' });
expect(gone(p)).toEqual(['11:spare']);
});
it('a later end dates the first spare day and removes nothing', () => {
const p = plan('2026-10-01', '2026-10-11');
expect(p.rows.slice(9)).toEqual([
{ id: 10, date: '2026-10-10' },
{ id: 11, date: '2026-10-11' },
{ id: 12, date: null },
]);
expect(p.removed).toEqual([]);
});
it('a range longer than all days adds new empty days after the spare ones', () => {
const p = plan('2026-10-01', '2026-10-14');
expect(ids(p)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, null, null]);
expect(p.rows.at(-1)).toEqual({ id: null, date: '2026-10-14' });
expect(p.removed).toEqual([]);
});
it('clearing the dates without a count keeps every day and takes all dates away', () => {
const p = plan(null, null);
expect(ids(p)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]);
expect(p.rows.every((r) => r.date === null)).toBe(true);
expect(p.removed).toEqual([]);
});
it('a stay from a removed day to a spare day goes with the removed day, so the spare day is empty', () => {
const p = plan('2026-10-01', '2026-10-09', [{ start_day_id: 10, end_day_id: 11 }]);
expect(gone(p)).toEqual(['10:overflow', '11:spare']);
});
it('a stay that stands keeps the spare day it checks out on', () => {
const p = plan('2026-10-01', '2026-10-09', [{ start_day_id: 9, end_day_id: 11 }]);
expect(gone(p)).toEqual(['10:overflow']);
expect(ids(p).slice(-2)).toEqual([11, 12]);
});
it('reads the order from the day numbers, whatever order the rows come in', () => {
const shuffled = [tenDays[4]!, tenDays[11]!, tenDays[0]!, tenDays[9]!, tenDays[10]!];
const p = planDayGrid({ days: shuffled, stays: [], startDate: '2026-11-01', endDate: '2026-11-02' });
expect(p.rows).toEqual([
{ id: 1, date: '2026-11-01' },
{ id: 5, date: '2026-11-02' },
{ id: 12, date: null },
]);
expect(gone(p)).toEqual(['10:overflow', '11:spare']);
});
describe('without dates', () => {
const undated = (n: number, planned: number[] = [], stays: DayGridStay[] = [], dayCount?: number) =>
planDayGrid({
days: Array.from({ length: n }, (_, i) => ({
id: i + 1,
day_number: i + 1,
date: i < 2 ? '2026-10-01' : null,
hasPlanItems: planned.includes(i + 1),
})),
stays,
startDate: null,
endDate: null,
dayCount,
});
it('trims to the count from the back, and only empty days', () => {
const p = undated(6, [5, 6], [{ start_day_id: 3, end_day_id: 3 }], 2);
expect(p.removed.map((r) => r.id)).toEqual([4, 2, 1]);
expect(p.removed.every((r) => r.reason === 'spare')).toBe(true);
expect(p.rows).toEqual([
{ id: 3, date: null },
{ id: 5, date: null },
{ id: 6, date: null },
]);
expect(p.removed.at(-1)).toMatchObject({ id: 1, date: '2026-10-01' });
});
it('adds empty days up to the count, and seven for a trip that has none', () => {
expect(undated(2, [], [], 4).rows).toEqual([
{ id: 1, date: null },
{ id: 2, date: null },
{ id: null, date: null },
{ id: null, date: null },
]);
expect(undated(0).rows).toHaveLength(7);
expect(undated(3, [], [], MAX_TRIP_DAYS + 1).rows).toHaveLength(MAX_TRIP_DAYS);
});
});
});
+153
View File
@@ -65,3 +65,156 @@ export function planDatedAppend(
const date = dayAfterLatest(trip.end_date, dayDates);
return { date, fits: tripSpanDays(trip.start_date, date) <= MAX_TRIP_DAYS };
}
/** The dates and day count an update leaves a trip with, and whether its day rows are rebuilt. */
export interface DayGridRange {
newStart: string | null | undefined;
newEnd: string | null | undefined;
/** A requested day count, held to 1..MAX_TRIP_DAYS; undefined when none came. */
dayCount: number | undefined;
/** True when a date moves or a day count arrives: the day rows are laid out anew. */
regenerate: boolean;
}
/**
* Where an update takes a trip's range. A field left out keeps the stored
* value, an explicit null clears it. The server rebuilds the day rows exactly
* when `regenerate` is true, and the trip dialog asks the same question before
* it saves, so a warning about lost days never fires for a save that loses none.
* Refusing a bad range stays with the server.
*/
export function resolveDayGridRange(
prev: { start_date?: string | null; end_date?: string | null },
data: { start_date?: string | null; end_date?: string | null; day_count?: number | null },
): DayGridRange {
const newStart = data.start_date !== undefined ? data.start_date : prev.start_date;
const newEnd = data.end_date !== undefined ? data.end_date : prev.end_date;
const dayCount = data.day_count ? Math.min(Math.max(Number(data.day_count) || 7, 1), MAX_TRIP_DAYS) : undefined;
const regenerate = newStart !== prev.start_date || newEnd !== prev.end_date || dayCount !== undefined;
return { newStart, newEnd, dayCount, regenerate };
}
/** A day row as the layout reads it. */
export interface DayGridDay {
id: number;
day_number: number;
date: string | null | undefined;
/** Places planned on the day, or notes on it. A day title or text does not count. */
hasPlanItems: boolean;
}
/** A stay by the two days it checks in and out on. */
export interface DayGridStay {
start_day_id: number;
end_day_id: number;
}
/**
* Why a day goes. `overflow`: a dated day past the new range, removed with
* everything on it. `spare`: a day with nothing planned that the new layout has
* no place for.
*/
export type DayGridRemovalReason = 'overflow' | 'spare';
export interface DayGridRemoval {
id: number;
/** Where the day stood before, and the date it had. */
day_number: number;
date: string | null;
reason: DayGridRemovalReason;
}
/** One row of the new layout: an existing day by its id, or a new one (null). */
export interface DayGridRow {
id: number | null;
date: string | null;
}
/** The day rows a trip ends up with, in order (index + 1 is the day number), and the days that go. */
export interface DayGridPlan {
rows: DayGridRow[];
removed: DayGridRemoval[];
}
const byDayNumber = (a: DayGridDay, b: DayGridDay): number => a.day_number - b.day_number;
const removal = (day: DayGridDay, reason: DayGridRemovalReason): DayGridRemoval => ({
id: day.id,
day_number: day.day_number,
date: day.date || null,
reason,
});
/** Nothing planned on the day and no stay checking in or out on it. */
const isEmptyDay = (day: DayGridDay, stays: readonly DayGridStay[]): boolean =>
!day.hasPlanItems && !stays.some((s) => s.start_day_id === day.id || s.end_day_id === day.id);
/**
* The day rows a trip gets for a range, the rule the server writes by
* (TripsService.generateDays carries this plan out) and the trip dialog warns
* by before it saves.
*
* With dates, plans follow their position: the n-th dated day takes the n-th
* date of the new range, so shortening a trip always takes the last dated days,
* with everything on them, even when it was the start that moved. Days without
* a date fill any dates left over, then new empty days do. Days without a date
* that are still over are dropped when empty and kept, undated, at the end when
* not. A stay on a dropped dated day goes with that day, so it no longer keeps a
* spare day it touched.
*
* Without dates every day loses its date and the trip is held to `dayCount`
* days (its current count when none is given): short of it, empty days are
* added at the end; over it, empty days go, the highest numbers first. A day
* with something planned on it is never dropped, even if that leaves the trip
* over the count.
*/
export function planDayGrid(input: {
days: readonly DayGridDay[];
stays: readonly DayGridStay[];
startDate: string | null | undefined;
endDate: string | null | undefined;
dayCount?: number;
}): DayGridPlan {
const { days, stays, startDate, endDate, dayCount } = input;
if (!startDate || !endDate) {
const ordered = [...days].sort(byDayNumber);
const target = Math.min(Math.max(dayCount ?? (ordered.length || 7), 1), MAX_TRIP_DAYS);
const surplus = ordered.length - target;
const removed =
surplus > 0
? ordered
.filter((day) => isEmptyDay(day, stays))
.reverse()
.slice(0, surplus)
.map((day) => removal(day, 'spare'))
: [];
const gone = new Set(removed.map((r) => r.id));
const rows: DayGridRow[] = ordered.filter((day) => !gone.has(day.id)).map((day) => ({ id: day.id, date: null }));
for (let i = 0; i < -surplus; i++) rows.push({ id: null, date: null });
return { rows, removed };
}
const dated = days.filter((day) => day.date).sort(byDayNumber);
const undated = days.filter((day) => !day.date).sort(byDayNumber);
const span = tripSpanDays(startDate, endDate);
const rows: DayGridRow[] = [];
let used = 0;
for (let i = 0; i < span; i++) {
const date = addIsoDays(startDate, i);
if (i < dated.length) rows.push({ id: dated[i]!.id, date });
else if (used < undated.length) rows.push({ id: undated[used++]!.id, date });
else rows.push({ id: null, date });
}
const overflow = dated.slice(Math.max(span, 0));
const gone = new Set(overflow.map((day) => day.id));
const standing = stays.filter((s) => !gone.has(s.start_day_id) && !gone.has(s.end_day_id));
const removed = overflow.map((day) => removal(day, 'overflow'));
for (const day of undated.slice(used)) {
if (isEmptyDay(day, standing)) removed.push(removal(day, 'spare'));
else rows.push({ id: day.id, date: null });
}
return { rows, removed };
}