fix: recurring event display and AI query improvements

- Use occurrenceStart instead of startTime in getEventsInRange so
  recurring events show their actual occurrence date to the AI
- Add lazy CalDAV sync in ChatService (syncOnce before first DB access)
- Add CaldavService.sync() with internal config check (silent no-op)
- Show German recurrence description (e.g. "Jede Woche") instead of
  generic "Wiederkehrend" in EventCardBase via formatRecurrenceRule()
- Move RepeatType and REPEAT_TYPE_LABELS from editEvent to shared
- Separate calendar overlay useFocusEffect from event loading
This commit is contained in:
2026-02-09 18:17:39 +01:00
parent 325246826a
commit 0a2aef2098
11 changed files with 105 additions and 29 deletions

View File

@@ -122,17 +122,22 @@ const Calendar = () => {
// Load events when tab gains focus or month/year changes
// NOTE: Wrapper needed because loadEvents is async (returns Promise)
// and useFocusEffect expects a sync function (optionally returning cleanup)
// Also re-open overlay if selectedDate exists (for back navigation from editEvent)
useFocusEffect(
useCallback(() => {
loadEvents();
if (selectedDate) {
setOverlayVisible(true);
}
const interval = setInterval(loadEvents, 10_000);
return () => clearInterval(interval);
}, [loadEvents, selectedDate]),
}, [loadEvents]),
);
// Re-open overlay after back navigation from editEvent
useFocusEffect(
useCallback(() => {
if (selectedDate) {
setOverlayVisible(true);
}
}, [selectedDate]),
);
// Group events by date (YYYY-MM-DD format)

View File

@@ -19,7 +19,7 @@ import { Ionicons } from "@expo/vector-icons";
import { ScrollableDropdown } from "../components/ScrollableDropdown";
import { useDropdownPosition } from "../hooks/useDropdownPosition";
import { EventService, ChatService } from "../services";
import { buildRRule, CreateEventDTO } from "@calchat/shared";
import { buildRRule, CreateEventDTO, REPEAT_TYPE_LABELS, RepeatType } from "@calchat/shared";
import { useChatStore } from "../stores";
import CustomTextInput, {
CustomTextInputProps,
@@ -86,15 +86,6 @@ const PickerRow = ({
);
};
type RepeatType = "Tag" | "Woche" | "Monat" | "Jahr";
const REPEAT_TYPE_LABELS: Record<RepeatType, string> = {
Tag: "Tage",
Woche: "Wochen",
Monat: "Monate",
Jahr: "Jahre",
};
type RepeatPressableProps = {
focused: boolean;
repeatType: RepeatType;

View File

@@ -19,7 +19,7 @@ export const EventCard = ({ event, onEdit, onDelete }: EventCardProps) => {
startTime={event.occurrenceStart}
endTime={event.occurrenceEnd}
description={event.description}
isRecurring={!!event.recurrenceRule}
recurrenceRule={event.recurrenceRule}
>
{/* Action buttons - TouchableOpacity with delayPressIn allows ScrollView to detect scroll gestures */}
<View className="flex-row justify-end mt-3 gap-3">

View File

@@ -8,6 +8,7 @@ import {
formatDateWithWeekday,
formatDateWithWeekdayShort,
formatTime,
formatRecurrenceRule,
} from "@calchat/shared";
type EventCardBaseProps = {
@@ -16,7 +17,7 @@ type EventCardBaseProps = {
startTime: Date;
endTime: Date;
description?: string;
isRecurring?: boolean;
recurrenceRule?: string;
children?: ReactNode;
};
@@ -46,7 +47,7 @@ export const EventCardBase = ({
startTime,
endTime,
description,
isRecurring,
recurrenceRule,
children,
}: EventCardBaseProps) => {
const { theme } = useThemeStore();
@@ -95,7 +96,7 @@ export const EventCardBase = ({
</View>
{/* Recurring indicator */}
{isRecurring && (
{recurrenceRule && (
<View className="flex-row items-center mb-1">
<Feather
name="repeat"
@@ -103,7 +104,9 @@ export const EventCardBase = ({
color={theme.textPrimary}
style={{ marginRight: 8 }}
/>
<Text style={{ color: theme.textPrimary }}>Wiederkehrend</Text>
<Text style={{ color: theme.textPrimary }}>
{formatRecurrenceRule(recurrenceRule)}
</Text>
</View>
)}

View File

@@ -113,7 +113,7 @@ export const ProposedEventCard = ({
startTime={event.startTime}
endTime={event.endTime}
description={event.description}
isRecurring={!!event.recurrenceRule}
recurrenceRule={event.recurrenceRule}
>
{/* Show new exception date for delete/single actions */}
{newExceptionDate && (

View File

@@ -234,7 +234,7 @@ export async function executeToolCall(
const eventsText = events
.map((e) => {
const start = new Date(e.startTime);
const start = new Date(e.occurrenceStart);
const recurrenceInfo = e.recurrenceRule ? " (wiederkehrend)" : "";
return `- ${e.title} (ID: ${e.id}) am ${formatDate(start)} um ${formatTime(start)} Uhr${recurrenceInfo}`;
})

View File

@@ -62,12 +62,13 @@ const aiProvider = new GPTAdapter();
// Initialize services
const authService = new AuthService(userRepo);
const eventService = new EventService(eventRepo);
const caldavService = new CaldavService(caldavRepo, eventService);
const chatService = new ChatService(
chatRepo,
eventService,
aiProvider,
caldavService,
);
const caldavService = new CaldavService(caldavRepo, eventService);
// Initialize controllers
const authController = new AuthController(authService);

View File

@@ -257,4 +257,14 @@ export class CaldavService {
async deleteConfig(userId: string) {
return await this.caldavRepo.deleteByUserId(userId);
}
/**
* Sync with CalDAV server if config exists. Silent no-op if no config.
*/
async sync(userId: string): Promise<void> {
const config = await this.getConfig(userId);
if (!config) return;
await this.pushAll(userId);
await this.pullEvents(userId);
}
}

View File

@@ -14,6 +14,7 @@ import {
} from "@calchat/shared";
import { ChatRepository, AIProvider } from "./interfaces";
import { EventService } from "./EventService";
import { CaldavService } from "./CaldavService";
import { getWeeksOverview, getMonthOverview } from "../utils/eventFormatters";
type TestResponse = {
@@ -543,6 +544,7 @@ export class ChatService {
private chatRepo: ChatRepository,
private eventService: EventService,
private aiProvider: AIProvider,
private caldavService: CaldavService,
) {}
async processMessage(
@@ -573,17 +575,32 @@ export class ChatService {
limit: 20,
});
// Lazy CalDAV sync: only sync once when the AI first accesses event data
let hasSynced = false;
const syncOnce = async () => {
if (hasSynced) return;
hasSynced = true;
try {
await this.caldavService.sync(userId);
} catch {
// CalDAV sync is not critical for AI responses
}
};
response = await this.aiProvider.processMessage(data.content, {
userId,
conversationHistory: history,
currentDate: new Date(),
fetchEventsInRange: async (start, end) => {
await syncOnce();
return this.eventService.getByDateRange(userId, start, end);
},
searchEvents: async (query) => {
await syncOnce();
return this.eventService.searchByTitle(userId, query);
},
fetchEventById: async (eventId) => {
await syncOnce();
return this.eventService.getById(eventId, userId);
},
});