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;