diff --git a/apps/mobile/src/__tests__/eventStore.test.ts b/apps/mobile/src/__tests__/eventStore.test.ts index b5741c9..655a9cd 100644 --- a/apps/mobile/src/__tests__/eventStore.test.ts +++ b/apps/mobile/src/__tests__/eventStore.test.ts @@ -183,6 +183,9 @@ describe('eventStore', () => { expect(settings).toEqual({ bufferMinutes: 30, enabled: true, + arrivalBufferMinutes: 5, + showWalkingOption: true, + showBikeOption: true, }); }); @@ -202,6 +205,9 @@ describe('eventStore', () => { const settings = { bufferMinutes: 45, enabled: false, + arrivalBufferMinutes: 5, + showWalkingOption: true, + showBikeOption: true, }; await saveNotificationSettings(settings); diff --git a/apps/mobile/src/__tests__/notifications.test.ts b/apps/mobile/src/__tests__/notifications.test.ts index d531b85..8c67ea1 100644 --- a/apps/mobile/src/__tests__/notifications.test.ts +++ b/apps/mobile/src/__tests__/notifications.test.ts @@ -32,7 +32,7 @@ describe('notifications service', () => { source: 'manual', }; - const leaveByTime = calculateLeaveByTime(event, [], 30); + const leaveByTime = calculateLeaveByTime(event, [], 5, 30); // Leave-by time should be 30 minutes before event time const expectedTime = new Date('2025-01-01T09:30:00Z'); @@ -75,7 +75,7 @@ describe('notifications service', () => { }, ]; - const leaveByTime = calculateLeaveByTime(event, journeys, 30); + const leaveByTime = calculateLeaveByTime(event, journeys, 5, 30); // Should use earliest non-cancelled journey (journey-2 at 07:00) minus buffer const expectedTime = new Date('2025-01-01T06:30:00Z'); @@ -118,7 +118,7 @@ describe('notifications service', () => { }, ]; - const leaveByTime = calculateLeaveByTime(event, journeys, 30); + const leaveByTime = calculateLeaveByTime(event, journeys, 5, 30); // Should use journey-2 since journey-1 is cancelled const expectedTime = new Date('2025-01-01T06:30:00Z'); @@ -149,7 +149,7 @@ describe('notifications service', () => { }, ]; - const leaveByTime = calculateLeaveByTime(event, journeys, 30); + const leaveByTime = calculateLeaveByTime(event, journeys, 5, 30); // All journeys cancelled, fall back to event time minus buffer const expectedTime = new Date('2025-01-01T09:30:00Z'); @@ -165,7 +165,7 @@ describe('notifications service', () => { source: 'manual', }; - const leaveByTime = calculateLeaveByTime(event, [], 0); + const leaveByTime = calculateLeaveByTime(event, [], 5, 0); expect(leaveByTime.getTime()).toBe(event.eventTime.getTime()); }); diff --git a/apps/mobile/src/screens/SettingsScreen.tsx b/apps/mobile/src/screens/SettingsScreen.tsx index 85dddb3..27b9972 100644 --- a/apps/mobile/src/screens/SettingsScreen.tsx +++ b/apps/mobile/src/screens/SettingsScreen.tsx @@ -41,6 +41,7 @@ export function SettingsScreen({ navigation }: ScreenProps) { showWalkingOption: true, showBikeOption: true, }); + const [showAdvanced, setShowAdvanced] = useState(false); const [locPermission, setLocPermission] = useState<'granted' | 'denied' | 'prompt'>('prompt'); const searchTimerRef = useRef | null>(null); @@ -102,7 +103,7 @@ export function SettingsScreen({ navigation }: ScreenProps) { // Search for stations near the user's actual GPS coordinates // Use Nominatim reverse geocode via the API to find a nearby station - const geoResults = await api.geocode('Wien', 'at'); + const geoResults = await api.reverseGeocode(userLat, userLng); // Find the station closest to the user's actual coordinates if (geoResults.length > 0) { const closest = geoResults.reduce((best: { lat: number; lng: number; display_name: string } | null, candidate) => { @@ -144,6 +145,34 @@ export function SettingsScreen({ navigation }: ScreenProps) { } }; + const updateArrivalBuffer = async (value: string) => { + const minutes = parseInt(value, 10); + if (!isNaN(minutes) && minutes >= 0) { + const updated = { ...notifSettings, arrivalBufferMinutes: minutes }; + setNotifSettings(updated); + await saveNotificationSettings(updated); + await rescheduleAllNotifications(); + } + }; + + const toggleWalking = async (value: boolean) => { + const updated = { ...notifSettings, showWalkingOption: value }; + setNotifSettings(updated); + await saveNotificationSettings(updated); + await rescheduleAllNotifications(); + }; + + const toggleBike = async (value: boolean) => { + const updated = { ...notifSettings, showBikeOption: value }; + setNotifSettings(updated); + await saveNotificationSettings(updated); + await rescheduleAllNotifications(); + }; + + const toggleAdvanced = () => { + setShowAdvanced(!showAdvanced); + }; + return ( {/* Origin Station */} @@ -199,6 +228,46 @@ export function SettingsScreen({ navigation }: ScreenProps) { Du wirst {notifSettings.bufferMinutes} Minuten vor der geplanten Abfahrt erinnert. + + + + {showAdvanced ? '↑ Weniger Optionen zeigen' : '↓ Mehr Optionen zeigen'} + + + + {showAdvanced && ( + + Ankunfts-Puffer (Minuten) + + Wie viele Minuten vor der Event-Zeit du am Ziel ankommen möchtest + + + Zu Fuß-Option anzeigen + + + + + Fahrrad-Option anzeigen + + + + )} ); @@ -235,6 +304,16 @@ const styles = StyleSheet.create({ }, locBtnText: { fontSize: 15, color: '#007AFF', fontWeight: '500' }, locStatus: { fontSize: 12, color: '#8e8e93', marginTop: 6 }, + advancedToggle: { + marginTop: 12, + marginBottom: 12, + }, + advancedSection: { + marginTop: 16, + paddingTop: 16, + borderTopWidth: 1, + borderTopColor: '#e5e5ea', + }, settingRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }, settingLabel: { fontSize: 14, color: '#1c1c1e' }, hint: { fontSize: 12, color: '#8e8e93', marginTop: 6 }, diff --git a/apps/mobile/src/services/notifications.ts b/apps/mobile/src/services/notifications.ts index 5110d78..df45f54 100644 --- a/apps/mobile/src/services/notifications.ts +++ b/apps/mobile/src/services/notifications.ts @@ -16,12 +16,22 @@ Notifications.setNotificationHandler({ /** * Calculate leave-by time from event time and journey data. * Uses earliest real departure time if journeys exist, otherwise event time minus buffer. + * + * @param event - The event to calculate leave-by time for + * @param journeys - Journey data for this event + * @param arrivalBufferMinutes - How many minutes before the event to arrive + * @param bufferMinutes - How many minutes before leaving to be reminded */ export function calculateLeaveByTime( event: Event, journeys: Journey[], + arrivalBufferMinutes: number, bufferMinutes: number ): Date { + // Calculate target arrival time (event time minus arrival buffer) + const targetArrivalTime = new Date(event.eventTime); + targetArrivalTime.setMinutes(targetArrivalTime.getMinutes() - arrivalBufferMinutes); + // If we have journeys, use the earliest non-cancelled real departure if (journeys.length > 0) { const best = journeys @@ -29,12 +39,13 @@ export function calculateLeaveByTime( .sort((a, b) => a.rD.getTime() - b.rD.getTime())[0]; if (best) { - return new Date(best.rD.getTime() - bufferMinutes * 60 * 1000); + // Leave by time = target arrival time - buffer minutes + return new Date(targetArrivalTime.getTime() - bufferMinutes * 60 * 1000); } } - // Fallback: event time minus buffer (no journey data) - return new Date(event.eventTime.getTime() - bufferMinutes * 60 * 1000); + // Fallback: event time minus arrival buffer minus buffer (no journey data) + return new Date(targetArrivalTime.getTime() - bufferMinutes * 60 * 1000); } /** @@ -55,7 +66,7 @@ export async function scheduleNotificationsForEvent( } // Calculate leave-by time (when user should actually leave) - const leaveByTime = calculateLeaveByTime(event, journeys, settings.bufferMinutes); + const leaveByTime = calculateLeaveByTime(event, journeys, settings.arrivalBufferMinutes, settings.bufferMinutes); // Cancel existing notifications for this event - cancel one by one const existing = await Notifications.getAllScheduledNotificationsAsync(); diff --git a/apps/mobile/src/store/eventStore.ts b/apps/mobile/src/store/eventStore.ts index 8827c8e..187f9c0 100644 --- a/apps/mobile/src/store/eventStore.ts +++ b/apps/mobile/src/store/eventStore.ts @@ -39,9 +39,13 @@ async function getNotificationSettings(): Promise { // Notification scheduling utilities // ──────────────────────────────────────────────────────────── -async function calculateLeaveByTime(event: Event, bufferMinutes: number): Promise { - // Fallback: event time minus buffer (no journey data) - return new Date(event.eventTime.getTime() - bufferMinutes * 60 * 1000); +async function calculateLeaveByTime(event: Event, arrivalBufferMinutes: number, bufferMinutes: number): Promise { + // Calculate target arrival time (event time minus arrival buffer) + const targetArrivalTime = new Date(event.eventTime); + targetArrivalTime.setMinutes(targetArrivalTime.getMinutes() - arrivalBufferMinutes); + + // Fallback: event time minus arrival buffer minus buffer (no journey data) + return new Date(targetArrivalTime.getTime() - bufferMinutes * 60 * 1000); } async function scheduleEventNotification(event: Event): Promise { @@ -50,9 +54,7 @@ async function scheduleEventNotification(event: Event): Promise { return; } - const leaveByTime = await calculateLeaveByTime(event, settings.bufferMinutes); - - // Cancel existing notifications for this event - cancel one by one + const leaveByTime = await calculateLeaveByTime(event, settings.arrivalBufferMinutes, settings.bufferMinutes); const existing = await Notifications.getAllScheduledNotificationsAsync(); const toCancel = existing.filter(n => n.content.data?.eventId === event.id); for (const notif of toCancel) { @@ -168,7 +170,7 @@ export async function rescheduleAllNotifications(): Promise { // Schedule new notifications for each event for (const event of events) { if (settings.enabled) { - const leaveByTime = await calculateLeaveByTime(event, settings.bufferMinutes); + const leaveByTime = await calculateLeaveByTime(event, settings.arrivalBufferMinutes, settings.bufferMinutes); // Default reminders: 30min, 10min, and at leave-by time const defaultReminders = [30, 10, 0];