diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 27bab6f5..326db6c1 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -58,7 +58,7 @@ jobs: # Commit and tag git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - git add server/package.json server/package-lock.json client/package.json + git add server/package.json server/package-lock.json client/package.json client/package-lock.json git commit -m "chore: bump version to $NEW_VERSION [skip ci]" git tag "v$NEW_VERSION" git push origin main --follow-tags diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fb161122..70eea819 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,11 +4,6 @@ permissions: contents: read on: - push: - branches: [main, dev] - paths: - - 'server/**' - - '.github/workflows/test.yml' pull_request: branches: [main, dev] paths: diff --git a/client/src/components/Admin/GitHubPanel.tsx b/client/src/components/Admin/GitHubPanel.tsx index 96a3b286..64b469ac 100644 --- a/client/src/components/Admin/GitHubPanel.tsx +++ b/client/src/components/Admin/GitHubPanel.tsx @@ -3,7 +3,7 @@ import { Tag, Calendar, ExternalLink, ChevronDown, ChevronUp, Loader2, Heart, Co import { getLocaleForLanguage, useTranslation } from '../../i18n' import apiClient from '../../api/client' -const REPO = 'mauriceboe/NOMAD' +const REPO = 'mauriceboe/TREK' const PER_PAGE = 10 export default function GitHubPanel() { diff --git a/client/src/i18n/translations/es.ts b/client/src/i18n/translations/es.ts index b1142fd2..4de28e2c 100644 --- a/client/src/i18n/translations/es.ts +++ b/client/src/i18n/translations/es.ts @@ -327,7 +327,7 @@ const es: Record = { 'login.signingIn': 'Iniciando sesión…', 'login.signIn': 'Entrar', 'login.createAdmin': 'Crear cuenta de administrador', - 'login.createAdminHint': 'Configura la primera cuenta administradora de NOMAD.', + 'login.createAdminHint': 'Configura la primera cuenta administradora de TREK.', 'login.setNewPassword': 'Establecer nueva contraseña', 'login.setNewPasswordHint': 'Debe cambiar su contraseña antes de continuar.', 'login.createAccount': 'Crear cuenta', @@ -483,7 +483,7 @@ const es: Record = { // Addons 'admin.tabs.addons': 'Complementos', 'admin.addons.title': 'Complementos', - 'admin.addons.subtitle': 'Activa o desactiva funciones para personalizar tu experiencia en NOMAD.', + 'admin.addons.subtitle': 'Activa o desactiva funciones para personalizar tu experiencia en TREK.', 'admin.addons.subtitleBefore': 'Activa o desactiva funciones para personalizar tu experiencia en ', 'admin.addons.subtitleAfter': '.', 'admin.addons.enabled': 'Activo', @@ -499,7 +499,7 @@ const es: Record = { 'admin.addons.noAddons': 'No hay complementos disponibles', 'admin.weather.title': 'Datos meteorológicos', 'admin.weather.badge': 'Desde el 24 de marzo de 2026', - 'admin.weather.description': 'NOMAD utiliza Open-Meteo como fuente de datos meteorológicos. Open-Meteo es un servicio meteorológico gratuito y de código abierto: no requiere clave API.', + 'admin.weather.description': 'TREK utiliza Open-Meteo como fuente de datos meteorológicos. Open-Meteo es un servicio meteorológico gratuito y de código abierto: no requiere clave API.', 'admin.weather.forecast': 'Pronóstico de 16 días', 'admin.weather.forecastDesc': 'Antes eran 5 días (OpenWeatherMap)', 'admin.weather.climate': 'Datos climáticos históricos', @@ -551,11 +551,11 @@ const es: Record = { 'admin.github.error': 'No se pudieron cargar las versiones', 'admin.github.by': 'por', 'admin.update.available': 'Actualización disponible', - 'admin.update.text': 'NOMAD {version} está disponible. Estás usando {current}.', + 'admin.update.text': 'TREK {version} está disponible. Estás usando {current}.', 'admin.update.button': 'Ver en GitHub', 'admin.update.install': 'Instalar actualización', 'admin.update.confirmTitle': '¿Instalar actualización?', - 'admin.update.confirmText': 'NOMAD se actualizará de {current} a {version}. Después, el servidor se reiniciará automáticamente.', + 'admin.update.confirmText': 'TREK se actualizará de {current} a {version}. Después, el servidor se reiniciará automáticamente.', 'admin.update.dataInfo': 'Todos tus datos (viajes, usuarios, claves API, subidas, Vacay, Atlas, presupuestos) se conservarán.', 'admin.update.warning': 'La app estará brevemente no disponible durante el reinicio.', 'admin.update.confirm': 'Actualizar ahora', @@ -565,7 +565,7 @@ const es: Record = { 'admin.update.backupHint': 'Recomendamos crear una copia de seguridad antes de actualizar.', 'admin.update.backupLink': 'Ir a Copia de seguridad', 'admin.update.howTo': 'Cómo actualizar', - 'admin.update.dockerText': 'Tu instancia de NOMAD se ejecuta en Docker. Para actualizar a {version}, ejecuta los siguientes comandos en tu servidor:', + 'admin.update.dockerText': 'Tu instancia de TREK se ejecuta en Docker. Para actualizar a {version}, ejecuta los siguientes comandos en tu servidor:', 'admin.update.reloadHint': 'Recarga la página en unos segundos.', // Vacay addon @@ -620,9 +620,9 @@ const es: Record = { 'vacay.carryOver': 'Arrastrar saldo', 'vacay.carryOverHint': 'Trasladar automáticamente los días restantes al año siguiente', 'vacay.sharing': 'Compartir', - 'vacay.sharingHint': 'Comparte tu calendario de vacaciones con otros usuarios de NOMAD', + 'vacay.sharingHint': 'Comparte tu calendario de vacaciones con otros usuarios de TREK', 'vacay.owner': 'Propietario', - 'vacay.shareEmailPlaceholder': 'Correo electrónico del usuario de NOMAD', + 'vacay.shareEmailPlaceholder': 'Correo electrónico del usuario de TREK', 'vacay.shareSuccess': 'Plan compartido correctamente', 'vacay.shareError': 'No se pudo compartir el plan', 'vacay.dissolve': 'Deshacer fusión', @@ -634,7 +634,7 @@ const es: Record = { 'vacay.noData': 'Sin datos', 'vacay.changeColor': 'Cambiar color', 'vacay.inviteUser': 'Invitar usuario', - 'vacay.inviteHint': 'Invita a otro usuario de NOMAD a compartir un calendario combinado de vacaciones.', + 'vacay.inviteHint': 'Invita a otro usuario de TREK a compartir un calendario combinado de vacaciones.', 'vacay.selectUser': 'Seleccionar usuario', 'vacay.sendInvite': 'Enviar invitación', 'vacay.inviteSent': 'Invitación enviada', diff --git a/client/src/pages/AdminPage.tsx b/client/src/pages/AdminPage.tsx index 45ef2b01..471338c9 100644 --- a/client/src/pages/AdminPage.tsx +++ b/client/src/pages/AdminPage.tsx @@ -1358,14 +1358,14 @@ export default function AdminPage(): React.ReactElement {
-{`docker pull mauriceboe/nomad:latest -docker stop nomad && docker rm nomad -docker run -d --name nomad \\ +{`docker pull mauriceboe/trek:latest +docker stop trek && docker rm trek +docker run -d --name trek \\ -p 3000:3000 \\ - -v /opt/nomad/data:/app/data \\ - -v /opt/nomad/uploads:/app/uploads \\ + -v /opt/trek/data:/app/data \\ + -v /opt/trek/uploads:/app/uploads \\ --restart unless-stopped \\ - mauriceboe/nomad:latest`} + mauriceboe/trek:latest`}
{ + async ({ tripId, title, type, reservation_time, location, confirmation_number, notes, day_id, place_id, start_day_id, end_day_id, check_in, check_out, assignment_id }) => { if (isDemoUser(userId)) return demoDenied(); if (!canAccessTrip(tripId, userId)) return noAccess(); @@ -542,8 +544,8 @@ export function registerTools(server: McpServer, userId: number): void { let accommodationId: number | null = null; if (type === 'hotel' && place_id && start_day_id && end_day_id) { const accResult = db.prepare( - 'INSERT INTO day_accommodations (trip_id, place_id, start_day_id, end_day_id, confirmation) VALUES (?, ?, ?, ?, ?)' - ).run(tripId, place_id, start_day_id, end_day_id, confirmation_number || null); + 'INSERT INTO day_accommodations (trip_id, place_id, start_day_id, end_day_id, check_in, check_out, confirmation) VALUES (?, ?, ?, ?, ?, ?, ?)' + ).run(tripId, place_id, start_day_id, end_day_id, check_in || null, check_out || null, confirmation_number || null); accommodationId = accResult.lastInsertRowid as number; } const result = db.prepare(` @@ -599,9 +601,11 @@ export function registerTools(server: McpServer, userId: number): void { place_id: z.number().int().positive().describe('The hotel place to link'), start_day_id: z.number().int().positive().describe('Check-in day ID'), end_day_id: z.number().int().positive().describe('Check-out day ID'), + check_in: z.string().max(10).optional().describe('Check-in time (e.g. "15:00")'), + check_out: z.string().max(10).optional().describe('Check-out time (e.g. "11:00")'), }, }, - async ({ tripId, reservationId, place_id, start_day_id, end_day_id }) => { + async ({ tripId, reservationId, place_id, start_day_id, end_day_id, check_in, check_out }) => { if (isDemoUser(userId)) return demoDenied(); if (!canAccessTrip(tripId, userId)) return noAccess(); const reservation = db.prepare('SELECT * FROM reservations WHERE id = ? AND trip_id = ?').get(reservationId, tripId) as Record | undefined; @@ -619,12 +623,12 @@ export function registerTools(server: McpServer, userId: number): void { const isNewAccommodation = !accommodationId; db.transaction(() => { if (accommodationId) { - db.prepare('UPDATE day_accommodations SET place_id = ?, start_day_id = ?, end_day_id = ? WHERE id = ?') - .run(place_id, start_day_id, end_day_id, accommodationId); + db.prepare('UPDATE day_accommodations SET place_id = ?, start_day_id = ?, end_day_id = ?, check_in = COALESCE(?, check_in), check_out = COALESCE(?, check_out) WHERE id = ?') + .run(place_id, start_day_id, end_day_id, check_in || null, check_out || null, accommodationId); } else { const accResult = db.prepare( - 'INSERT INTO day_accommodations (trip_id, place_id, start_day_id, end_day_id, confirmation) VALUES (?, ?, ?, ?, ?)' - ).run(tripId, place_id, start_day_id, end_day_id, reservation.confirmation_number || null); + 'INSERT INTO day_accommodations (trip_id, place_id, start_day_id, end_day_id, check_in, check_out, confirmation) VALUES (?, ?, ?, ?, ?, ?, ?)' + ).run(tripId, place_id, start_day_id, end_day_id, check_in || null, check_out || null, reservation.confirmation_number || null); accommodationId = accResult.lastInsertRowid as number; } db.prepare('UPDATE reservations SET place_id = ?, accommodation_id = ? WHERE id = ?') diff --git a/server/src/routes/trips.ts b/server/src/routes/trips.ts index b35fb1a7..c0f82674 100644 --- a/server/src/routes/trips.ts +++ b/server/src/routes/trips.ts @@ -74,9 +74,26 @@ 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, start_date, end_date, currency, reminder_days } = req.body; + const { title, description, currency, reminder_days } = req.body; if (!title) return res.status(400).json({ error: 'Title is required' }); - if (start_date && end_date && new Date(end_date) < new Date(start_date)) + + const toDateStr = (d: Date) => d.toISOString().slice(0, 10); + const addDays = (d: Date, n: number) => { const r = new Date(d); r.setDate(r.getDate() + n); return r; }; + + let start_date: string | null = req.body.start_date || null; + 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)); + } else if (start_date && !end_date) { + end_date = toDateStr(addDays(new Date(start_date), 7)); + } else if (!start_date && end_date) { + start_date = toDateStr(addDays(new Date(end_date), -7)); + } + + if (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 }); diff --git a/server/tests/integration/trips.test.ts b/server/tests/integration/trips.test.ts index 6c619d73..16d224d8 100644 --- a/server/tests/integration/trips.test.ts +++ b/server/tests/integration/trips.test.ts @@ -89,9 +89,15 @@ describe('Create trip', () => { expect(days[4].date).toBe('2026-06-05'); }); - it('TRIP-002 — POST /api/trips without dates returns 201 and no date-specific days', async () => { + it('TRIP-002 — POST /api/trips without dates returns 201 and defaults to a 7-day window', async () => { const { user } = createUser(testDb); + const addDays = (d: Date, n: number) => { const r = new Date(d); r.setDate(r.getDate() + n); return r; }; + const toDateStr = (d: Date) => d.toISOString().slice(0, 10); + const tomorrow = addDays(new Date(), 1); + const expectedStart = toDateStr(tomorrow); + const expectedEnd = toDateStr(addDays(tomorrow, 7)); + const res = await request(app) .post('/api/trips') .set('Cookie', authCookie(user.id)) @@ -99,12 +105,12 @@ describe('Create trip', () => { expect(res.status).toBe(201); expect(res.body.trip).toBeDefined(); - expect(res.body.trip.start_date).toBeNull(); - expect(res.body.trip.end_date).toBeNull(); + expect(res.body.trip.start_date).toBe(expectedStart); + expect(res.body.trip.end_date).toBe(expectedEnd); - // Days with explicit dates should not be present + // Should have 8 days (start through end inclusive) const daysWithDate = testDb.prepare('SELECT * FROM days WHERE trip_id = ? AND date IS NOT NULL').all(res.body.trip.id) as any[]; - expect(daysWithDate).toHaveLength(0); + expect(daysWithDate).toHaveLength(8); }); it('TRIP-001 — POST /api/trips requires a title, returns 400 without one', async () => {