diff --git a/client/src/components/Trips/TripFormModal.tsx b/client/src/components/Trips/TripFormModal.tsx
index aff4012..7643105 100644
--- a/client/src/components/Trips/TripFormModal.tsx
+++ b/client/src/components/Trips/TripFormModal.tsx
@@ -36,6 +36,7 @@ export default function TripFormModal({ isOpen, onClose, onSave, trip, onCoverUp
start_date: '',
end_date: '',
reminder_days: 0 as number,
+ day_count: 7,
})
const [customReminder, setCustomReminder] = useState(false)
const [error, setError] = useState('')
@@ -56,11 +57,12 @@ export default function TripFormModal({ isOpen, onClose, onSave, trip, onCoverUp
start_date: trip.start_date || '',
end_date: trip.end_date || '',
reminder_days: rd,
+ day_count: trip.day_count || 7,
})
setCustomReminder(![0, 1, 3, 9].includes(rd))
setCoverPreview(trip.cover_image || null)
} else {
- setFormData({ title: '', description: '', start_date: '', end_date: '', reminder_days: tripRemindersEnabled ? 3 : 0 })
+ setFormData({ title: '', description: '', start_date: '', end_date: '', reminder_days: tripRemindersEnabled ? 3 : 0, day_count: 7 })
setCustomReminder(false)
setCoverPreview(null)
}
@@ -98,6 +100,7 @@ export default function TripFormModal({ isOpen, onClose, onSave, trip, onCoverUp
start_date: formData.start_date || null,
end_date: formData.end_date || null,
reminder_days: formData.reminder_days,
+ ...(!formData.start_date && !formData.end_date ? { day_count: formData.day_count } : {}),
})
// Add selected members for newly created trips
if (selectedMembers.length > 0 && result?.trip?.id) {
@@ -297,6 +300,18 @@ export default function TripFormModal({ isOpen, onClose, onSave, trip, onCoverUp
+ {!formData.start_date && !formData.end_date && (
+
+
+
update('day_count', Math.max(1, Math.min(365, Number(e.target.value) || 1)))}
+ className={inputCls} />
+
{t('dashboard.dayCountHint')}
+
+ )}
+
{/* Reminder — only visible to owner (or when creating) */}
{(!isEditing || trip?.user_id === currentUser?.id || currentUser?.role === 'admin') && (
diff --git a/client/src/i18n/translations/ar.ts b/client/src/i18n/translations/ar.ts
index 4f1a81b..6bd8b31 100644
--- a/client/src/i18n/translations/ar.ts
+++ b/client/src/i18n/translations/ar.ts
@@ -118,6 +118,8 @@ const ar: Record = {
'dashboard.tripDescriptionPlaceholder': 'عمّ تتحدث هذه الرحلة؟',
'dashboard.startDate': 'تاريخ البداية',
'dashboard.endDate': 'تاريخ النهاية',
+ 'dashboard.dayCount': 'عدد الأيام',
+ 'dashboard.dayCountHint': 'عدد الأيام المراد التخطيط لها عندما لا يتم تحديد تواريخ السفر.',
'dashboard.noDateHint': 'لا يوجد تاريخ محدد. سيتم إنشاء 7 أيام افتراضية ويمكنك تغيير ذلك لاحقًا.',
'dashboard.coverImage': 'صورة الغلاف',
'dashboard.addCoverImage': 'إضافة صورة غلاف',
diff --git a/client/src/i18n/translations/br.ts b/client/src/i18n/translations/br.ts
index 425215a..65ecdae 100644
--- a/client/src/i18n/translations/br.ts
+++ b/client/src/i18n/translations/br.ts
@@ -113,6 +113,8 @@ const br: Record = {
'dashboard.tripDescriptionPlaceholder': 'Sobre o que é esta viagem?',
'dashboard.startDate': 'Data de início',
'dashboard.endDate': 'Data de término',
+ 'dashboard.dayCount': 'Número de dias',
+ 'dashboard.dayCountHint': 'Quantos dias planejar quando nenhuma data de viagem for definida.',
'dashboard.noDateHint': 'Sem datas — serão criados 7 dias padrão. Você pode alterar depois.',
'dashboard.coverImage': 'Imagem de capa',
'dashboard.addCoverImage': 'Adicionar capa (ou arrastar e soltar)',
diff --git a/client/src/i18n/translations/cs.ts b/client/src/i18n/translations/cs.ts
index 3e06555..31a8fd4 100644
--- a/client/src/i18n/translations/cs.ts
+++ b/client/src/i18n/translations/cs.ts
@@ -114,6 +114,8 @@ const cs: Record = {
'dashboard.tripDescriptionPlaceholder': 'O čem je tato cesta?',
'dashboard.startDate': 'Datum začátku',
'dashboard.endDate': 'Datum konce',
+ 'dashboard.dayCount': 'Počet dnů',
+ 'dashboard.dayCountHint': 'Kolik dnů naplánovat, když nejsou nastavena data cesty.',
'dashboard.noDateHint': 'Datum nezadáno – výchozí délka nastavena na 7 dní. Toto lze kdykoli změnit.',
'dashboard.coverImage': 'Úvodní obrázek',
'dashboard.addCoverImage': 'Vybrat úvodní obrázek (nebo přetáhnout sem)',
diff --git a/client/src/i18n/translations/de.ts b/client/src/i18n/translations/de.ts
index a7af9ed..6716779 100644
--- a/client/src/i18n/translations/de.ts
+++ b/client/src/i18n/translations/de.ts
@@ -113,6 +113,8 @@ const de: Record = {
'dashboard.tripDescriptionPlaceholder': 'Worum geht es bei dieser Reise?',
'dashboard.startDate': 'Startdatum',
'dashboard.endDate': 'Enddatum',
+ 'dashboard.dayCount': 'Anzahl Tage',
+ 'dashboard.dayCountHint': 'Wie viele Tage geplant werden sollen, wenn kein Reisezeitraum gesetzt ist.',
'dashboard.noDateHint': 'Kein Datum gesetzt — es werden 7 Standardtage erstellt. Du kannst das jederzeit ändern.',
'dashboard.coverImage': 'Titelbild',
'dashboard.addCoverImage': 'Titelbild hinzufügen (oder per Drag & Drop)',
diff --git a/client/src/i18n/translations/en.ts b/client/src/i18n/translations/en.ts
index 6974c96..c0f3997 100644
--- a/client/src/i18n/translations/en.ts
+++ b/client/src/i18n/translations/en.ts
@@ -113,6 +113,8 @@ const en: Record = {
'dashboard.tripDescriptionPlaceholder': 'What is this trip about?',
'dashboard.startDate': 'Start Date',
'dashboard.endDate': 'End Date',
+ 'dashboard.dayCount': 'Number of Days',
+ 'dashboard.dayCountHint': 'How many days to plan for when no travel dates are set.',
'dashboard.noDateHint': 'No date set — 7 default days will be created. You can change this anytime.',
'dashboard.coverImage': 'Cover Image',
'dashboard.addCoverImage': 'Add cover image (or drag & drop)',
diff --git a/client/src/i18n/translations/es.ts b/client/src/i18n/translations/es.ts
index ee9eb42..6c35e6c 100644
--- a/client/src/i18n/translations/es.ts
+++ b/client/src/i18n/translations/es.ts
@@ -114,6 +114,8 @@ const es: Record = {
'dashboard.tripDescriptionPlaceholder': '¿De qué trata este viaje?',
'dashboard.startDate': 'Fecha de inicio',
'dashboard.endDate': 'Fecha de fin',
+ 'dashboard.dayCount': 'Número de días',
+ 'dashboard.dayCountHint': 'Cuántos días planificar cuando no se han establecido fechas de viaje.',
'dashboard.noDateHint': 'Sin fecha definida: se crearán 7 días por defecto. Puedes cambiarlo cuando quieras.',
'dashboard.coverImage': 'Imagen de portada',
'dashboard.addCoverImage': 'Añadir imagen de portada',
diff --git a/client/src/i18n/translations/fr.ts b/client/src/i18n/translations/fr.ts
index 88390b9..7416c7a 100644
--- a/client/src/i18n/translations/fr.ts
+++ b/client/src/i18n/translations/fr.ts
@@ -113,6 +113,8 @@ const fr: Record = {
'dashboard.tripDescriptionPlaceholder': 'De quoi parle ce voyage ?',
'dashboard.startDate': 'Date de début',
'dashboard.endDate': 'Date de fin',
+ 'dashboard.dayCount': 'Nombre de jours',
+ 'dashboard.dayCountHint': 'Nombre de jours à planifier lorsqu'aucune date de voyage n'est définie.',
'dashboard.noDateHint': 'Aucune date définie — 7 jours par défaut seront créés. Vous pouvez modifier cela à tout moment.',
'dashboard.coverImage': 'Image de couverture',
'dashboard.addCoverImage': 'Ajouter une image de couverture',
diff --git a/client/src/i18n/translations/hu.ts b/client/src/i18n/translations/hu.ts
index 78e1aea..af7aa8e 100644
--- a/client/src/i18n/translations/hu.ts
+++ b/client/src/i18n/translations/hu.ts
@@ -113,6 +113,8 @@ const hu: Record = {
'dashboard.tripDescriptionPlaceholder': 'Miről szól ez az utazás?',
'dashboard.startDate': 'Kezdő dátum',
'dashboard.endDate': 'Záró dátum',
+ 'dashboard.dayCount': 'Napok száma',
+ 'dashboard.dayCountHint': 'Hány napot tervezzen, ha nincsenek utazási dátumok megadva.',
'dashboard.noDateHint': 'Nincs dátum megadva — 7 alapértelmezett nap jön létre. Ezt bármikor módosíthatod.',
'dashboard.coverImage': 'Borítókép',
'dashboard.addCoverImage': 'Borítókép hozzáadása',
diff --git a/client/src/i18n/translations/it.ts b/client/src/i18n/translations/it.ts
index 79b7c4d..5852db5 100644
--- a/client/src/i18n/translations/it.ts
+++ b/client/src/i18n/translations/it.ts
@@ -113,6 +113,8 @@ const it: Record = {
'dashboard.tripDescriptionPlaceholder': 'Di cosa tratta questo viaggio?',
'dashboard.startDate': 'Data di inizio',
'dashboard.endDate': 'Data di fine',
+ 'dashboard.dayCount': 'Numero di giorni',
+ 'dashboard.dayCountHint': 'Quanti giorni pianificare quando non sono impostate date di viaggio.',
'dashboard.noDateHint': 'Nessuna data impostata — verranno creati 7 giorni predefiniti. Puoi cambiarlo in qualsiasi momento.',
'dashboard.coverImage': 'Immagine di copertina',
'dashboard.addCoverImage': 'Aggiungi immagine di copertina (o trascinala qui)',
diff --git a/client/src/i18n/translations/nl.ts b/client/src/i18n/translations/nl.ts
index c8d70de..b36a2ed 100644
--- a/client/src/i18n/translations/nl.ts
+++ b/client/src/i18n/translations/nl.ts
@@ -113,6 +113,8 @@ const nl: Record = {
'dashboard.tripDescriptionPlaceholder': 'Waar gaat deze reis over?',
'dashboard.startDate': 'Startdatum',
'dashboard.endDate': 'Einddatum',
+ 'dashboard.dayCount': 'Aantal dagen',
+ 'dashboard.dayCountHint': 'Hoeveel dagen te plannen wanneer er geen reisdata zijn ingesteld.',
'dashboard.noDateHint': 'Geen datum ingesteld — er worden standaard 7 dagen aangemaakt. Je kunt dit altijd wijzigen.',
'dashboard.coverImage': 'Omslagafbeelding',
'dashboard.addCoverImage': 'Omslagafbeelding toevoegen',
diff --git a/client/src/i18n/translations/pl.ts b/client/src/i18n/translations/pl.ts
index d5e7e0e..7a65ad6 100644
--- a/client/src/i18n/translations/pl.ts
+++ b/client/src/i18n/translations/pl.ts
@@ -99,6 +99,8 @@ const pl: Record = {
'dashboard.tripDescriptionPlaceholder': 'Opisz swoją podróż',
'dashboard.startDate': 'Data rozpoczęcia',
'dashboard.endDate': 'Data zakończenia',
+ 'dashboard.dayCount': 'Liczba dni',
+ 'dashboard.dayCountHint': 'Ile dni zaplanować, gdy nie ustawiono dat podróży.',
'dashboard.noDateHint': 'Nie ustawiono daty — zostanie utworzonych 7 domyślnych dni. Możesz to zmienić w dowolnym momencie.',
'dashboard.coverImage': 'Okładka',
'dashboard.addCoverImage': 'Dodaj okładkę (lub przeciągnij i upuść)',
diff --git a/client/src/i18n/translations/ru.ts b/client/src/i18n/translations/ru.ts
index 4fec02e..5b1448c 100644
--- a/client/src/i18n/translations/ru.ts
+++ b/client/src/i18n/translations/ru.ts
@@ -113,6 +113,8 @@ const ru: Record = {
'dashboard.tripDescriptionPlaceholder': 'О чём эта поездка?',
'dashboard.startDate': 'Дата начала',
'dashboard.endDate': 'Дата окончания',
+ 'dashboard.dayCount': 'Количество дней',
+ 'dashboard.dayCountHint': 'Сколько дней планировать, если даты поездки не указаны.',
'dashboard.noDateHint': 'Дата не указана — будет создано 7 дней по умолчанию. Вы можете изменить это в любое время.',
'dashboard.coverImage': 'Обложка',
'dashboard.addCoverImage': 'Добавить обложку',
diff --git a/client/src/i18n/translations/zh.ts b/client/src/i18n/translations/zh.ts
index 9d27c3a..192a339 100644
--- a/client/src/i18n/translations/zh.ts
+++ b/client/src/i18n/translations/zh.ts
@@ -113,6 +113,8 @@ const zh: Record = {
'dashboard.tripDescriptionPlaceholder': '这次旅行是关于什么的?',
'dashboard.startDate': '开始日期',
'dashboard.endDate': '结束日期',
+ 'dashboard.dayCount': '天数',
+ 'dashboard.dayCountHint': '未设置旅行日期时要规划的天数。',
'dashboard.noDateHint': '未设置日期——将默认创建 7 天。你可以随时修改。',
'dashboard.coverImage': '封面图片',
'dashboard.addCoverImage': '添加封面图片',
diff --git a/server/src/routes/trips.ts b/server/src/routes/trips.ts
index b9225b7..b9d7b94 100644
--- a/server/src/routes/trips.ts
+++ b/server/src/routes/trips.ts
@@ -74,7 +74,7 @@ router.post('/', authenticate, (req: Request, res: Response) => {
if (!checkPermission('trip_create', authReq.user.role, null, authReq.user.id, false))
return res.status(403).json({ error: 'No permission to create trips' });
- const { title, description, currency, reminder_days } = req.body;
+ const { title, description, currency, reminder_days, day_count } = req.body;
if (!title) return res.status(400).json({ error: 'Title is required' });
const toDateStr = (d: Date) => d.toISOString().slice(0, 10);
@@ -84,19 +84,18 @@ router.post('/', authenticate, (req: Request, res: Response) => {
let end_date: string | null = req.body.end_date || null;
if (!start_date && !end_date) {
- const tomorrow = addDays(new Date(), 1);
- start_date = toDateStr(tomorrow);
- end_date = toDateStr(addDays(tomorrow, 7));
+ // No dates: create dateless placeholder days (day_count or default 7)
} else if (start_date && !end_date) {
- end_date = toDateStr(addDays(new Date(start_date), 7));
+ end_date = toDateStr(addDays(new Date(start_date), 6));
} else if (!start_date && end_date) {
- start_date = toDateStr(addDays(new Date(end_date), -7));
+ start_date = toDateStr(addDays(new Date(end_date), -6));
}
- if (new Date(end_date!) < new Date(start_date!))
+ if (start_date && end_date && new Date(end_date) < new Date(start_date))
return res.status(400).json({ error: 'End date must be after start date' });
- const { trip, tripId, reminderDays } = createTrip(authReq.user.id, { title, description, start_date, end_date, currency, reminder_days });
+ const parsedDayCount = day_count ? Math.min(Math.max(Number(day_count) || 7, 1), 365) : undefined;
+ const { trip, tripId, reminderDays } = createTrip(authReq.user.id, { title, description, start_date, end_date, currency, reminder_days, day_count: parsedDayCount });
writeAudit({ userId: authReq.user.id, action: 'trip.create', ip: getClientIp(req), details: { tripId, title, reminder_days: reminderDays === 0 ? 'none' : `${reminderDays} days` } });
if (reminderDays > 0) {
@@ -136,7 +135,7 @@ router.put('/:id', authenticate, (req: Request, res: Response) => {
return res.status(403).json({ error: 'No permission to change cover image' });
}
// General edit check (title, description, dates, currency, reminder_days)
- const editFields = ['title', 'description', 'start_date', 'end_date', 'currency', 'reminder_days'];
+ const editFields = ['title', 'description', 'start_date', 'end_date', 'currency', 'reminder_days', 'day_count'];
if (editFields.some(f => req.body[f] !== undefined)) {
if (!checkPermission('trip_edit', authReq.user.role, tripOwnerId, authReq.user.id, isMember))
return res.status(403).json({ error: 'No permission to edit this trip' });
diff --git a/server/src/services/tripService.ts b/server/src/services/tripService.ts
index 42a0d53..9afd7f2 100644
--- a/server/src/services/tripService.ts
+++ b/server/src/services/tripService.ts
@@ -32,7 +32,7 @@ export { isOwner };
// ── Day generation ────────────────────────────────────────────────────────
-export function generateDays(tripId: number | bigint | string, startDate: string | null, endDate: string | null, maxDays?: number) {
+export function generateDays(tripId: number | bigint | string, startDate: string | null, endDate: string | null, maxDays?: number, dayCount?: number) {
const existing = db.prepare('SELECT id, day_number, date FROM days WHERE trip_id = ?').all(tripId) as { id: number; day_number: number; date: string | null }[];
if (!startDate || !endDate) {
@@ -41,12 +41,13 @@ export function generateDays(tripId: number | bigint | string, startDate: string
if (withDates.length > 0) {
db.prepare(`DELETE FROM days WHERE trip_id = ? AND date IS NOT NULL`).run(tripId);
}
- const needed = 7 - datelessExisting.length;
+ const targetCount = Math.min(Math.max(dayCount ?? (datelessExisting.length || 7), 1), MAX_TRIP_DAYS);
+ const needed = targetCount - datelessExisting.length;
if (needed > 0) {
const insert = db.prepare('INSERT INTO days (trip_id, day_number, date) VALUES (?, ?, NULL)');
for (let i = 0; i < needed; i++) insert.run(tripId, datelessExisting.length + i + 1);
} else if (needed < 0) {
- const toRemove = datelessExisting.slice(7);
+ const toRemove = datelessExisting.slice(targetCount);
const del = db.prepare('DELETE FROM days WHERE id = ?');
for (const d of toRemove) del.run(d.id);
}
@@ -139,6 +140,7 @@ interface CreateTripData {
end_date?: string | null;
currency?: string;
reminder_days?: number;
+ day_count?: number;
}
export function createTrip(userId: number, data: CreateTripData, maxDays?: number) {
@@ -152,7 +154,7 @@ export function createTrip(userId: number, data: CreateTripData, maxDays?: numbe
`).run(userId, data.title, data.description || null, data.start_date || null, data.end_date || null, data.currency || 'EUR', rd);
const tripId = result.lastInsertRowid;
- generateDays(tripId, data.start_date || null, data.end_date || null, maxDays);
+ generateDays(tripId, data.start_date || null, data.end_date || null, maxDays, data.day_count);
const trip = db.prepare(`${TRIP_SELECT} WHERE t.id = :tripId`).get({ userId, tripId });
return { trip, tripId: Number(tripId), reminderDays: rd };
@@ -175,6 +177,7 @@ interface UpdateTripData {
is_archived?: boolean | number;
cover_image?: string;
reminder_days?: number;
+ day_count?: number;
}
export interface UpdateTripResult {
@@ -214,8 +217,9 @@ export function updateTrip(tripId: string | number, userId: number, data: Update
WHERE id=?
`).run(newTitle, newDesc, newStart || null, newEnd || null, newCurrency, newArchived, newCover, newReminder, tripId);
- if (newStart !== trip.start_date || newEnd !== trip.end_date)
- generateDays(tripId, newStart || null, newEnd || null);
+ const dayCount = data.day_count ? Math.min(Math.max(Number(data.day_count) || 7, 1), MAX_TRIP_DAYS) : undefined;
+ if (newStart !== trip.start_date || newEnd !== trip.end_date || dayCount)
+ generateDays(tripId, newStart || null, newEnd || null, undefined, dayCount);
const changes: Record = {};
if (title && title !== trip.title) changes.title = title;