Files
heval/app/(tabs)/calendar.tsx
2025-07-15 20:53:11 +02:00

342 lines
9.6 KiB
TypeScript

import { Ionicons } from "@expo/vector-icons";
import * as Calendar from "expo-calendar";
import * as SQLite from "expo-sqlite";
import React, { useEffect, useState } from "react";
import {
Button,
FlatList,
Modal,
SafeAreaView,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
} from "react-native";
export interface EventItem {
id: string;
title: string;
startDate: string;
endDate: string;
}
const db = SQLite.openDatabaseSync("events.db");
export default function CalendarTab() {
const [source, setSource] = useState<"sqlite" | "iphone">("sqlite");
const [events, setEvents] = useState<EventItem[]>([]);
const [loading, setLoading] = useState(false);
const [selectedYear, setSelectedYear] = useState<number>(
new Date().getFullYear()
);
const [permissionError, setPermissionError] = useState<string | null>(null);
const [calendarType, setCalendarType] = useState<"calendar" | "reminder">(
"calendar"
);
const [modalVisible, setModalVisible] = useState(false);
const [newTitle, setNewTitle] = useState("");
const [newStart, setNewStart] = useState("");
const [newEnd, setNewEnd] = useState("");
useEffect(() => {
if (source === "sqlite") {
loadEventsFromDB();
} else {
loadEventsFromDevice();
}
}, [source, selectedYear, calendarType]);
const loadEventsFromDB = async () => {
setLoading(true);
try {
const stmt = await db.prepareAsync("SELECT * FROM events;");
const loadedEvents: EventItem[] = [];
const result = await stmt.executeAsync([]);
for await (const row of result) {
loadedEvents.push(row as EventItem);
}
setEvents(loadedEvents);
await stmt.finalizeAsync();
} catch (e) {
setEvents([]);
} finally {
setLoading(false);
}
};
const loadEventsFromDevice = async () => {
setLoading(true);
setPermissionError(null);
try {
let perm;
if (calendarType === "reminder") {
perm = await Calendar.requestRemindersPermissionsAsync();
} else {
perm = await Calendar.requestCalendarPermissionsAsync();
}
if (!perm.granted) {
setEvents([]);
setPermissionError(
`Keine Berechtigung für ${
calendarType === "reminder" ? "Erinnerungen" : "Kalender"
}-Zugriff. Bitte in den iOS-Einstellungen aktivieren.`
);
return;
}
let calendars = await Calendar.getCalendarsAsync();
console.log(
"Gefundene Kalender:",
calendars.map((c) => ({ id: c.id, title: c.title, type: c.type }))
);
if (calendarType === "calendar") {
calendars = calendars.filter((c) => String(c.type) === "calendar");
} else {
calendars = calendars.filter((c) => String(c.type) === "reminder");
}
const start = new Date(selectedYear, 0, 1);
const end = new Date(selectedYear, 11, 31, 23, 59, 59);
let allEvents: EventItem[] = [];
for (const cal of calendars) {
const calEvents = await Calendar.getEventsAsync([cal.id], start, end);
console.log(
`Events für Kalender ${cal.title}:`,
calEvents.map((e) => ({
id: e.id,
title: e.title,
start: e.startDate,
end: e.endDate,
}))
);
allEvents = allEvents.concat(
calEvents.map((e) => ({
id: e.id,
title: e.title || "(Kein Titel)",
startDate:
typeof e.startDate === "string"
? e.startDate
: new Date(e.startDate).toISOString(),
endDate:
typeof e.endDate === "string"
? e.endDate
: new Date(e.endDate).toISOString(),
}))
);
}
setEvents(allEvents);
} catch (e: any) {
setEvents([]);
setPermissionError(
"Fehler beim Zugriff auf " +
(calendarType === "reminder" ? "Erinnerungen" : "Kalender") +
": " +
(e?.message || JSON.stringify(e))
);
} finally {
setLoading(false);
}
};
const addEventToDB = async () => {
if (!newTitle || !newStart || !newEnd) return;
await db.execAsync(
`INSERT INTO events (id, title, startDate, endDate) VALUES ('${Date.now()}', '${newTitle.replace(
/'/g,
"''"
)}', '${newStart}', '${newEnd}');`
);
setModalVisible(false);
setNewTitle("");
setNewStart("");
setNewEnd("");
await loadEventsFromDB();
};
const renderItem = ({ item }: { item: EventItem }) => (
<View style={styles.item}>
<Text style={styles.title}>{item.title}</Text>
<Text style={styles.time}>
{new Date(item.startDate).toLocaleString()} -{" "}
{new Date(item.endDate).toLocaleString()}
</Text>
</View>
);
return (
<SafeAreaView style={styles.container}>
<View
style={{
flexDirection: "row",
justifyContent: "flex-end",
alignItems: "center",
}}
>
<TouchableOpacity
onPress={() => setModalVisible(true)}
style={{ padding: 10 }}
>
<Ionicons name="add-circle" size={32} color="#007AFF" />
</TouchableOpacity>
</View>
<View style={styles.switchRow}>
<TouchableOpacity
style={[
styles.switchButton,
source === "sqlite" && styles.activeButton,
]}
onPress={() => setSource("sqlite")}
>
<Text style={styles.switchText}>SQLite</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.switchButton,
source === "iphone" && styles.activeButton,
]}
onPress={() => setSource("iphone")}
>
<Text style={styles.switchText}>
iPhone ({calendarType === "calendar" ? "Kalender" : "Erinnerungen"})
</Text>
</TouchableOpacity>
</View>
{source === "iphone" && (
<View style={styles.switchRow}>
<TouchableOpacity
style={[
styles.switchButton,
calendarType === "calendar" && styles.activeButton,
]}
onPress={() => setCalendarType("calendar")}
>
<Text style={styles.switchText}>Kalender</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.switchButton,
calendarType === "reminder" && styles.activeButton,
]}
onPress={() => setCalendarType("reminder")}
>
<Text style={styles.switchText}>Erinnerungen</Text>
</TouchableOpacity>
</View>
)}
<Text style={styles.header}>Kalender Events ({selectedYear})</Text>
<FlatList
data={events}
keyExtractor={(item) => item.id}
renderItem={renderItem}
ListEmptyComponent={
permissionError ? (
<Text style={{ textAlign: "center", color: "red" }}>
{permissionError}
</Text>
) : (
<Text style={{ textAlign: "center" }}>Keine Events gefunden.</Text>
)
}
refreshing={loading}
onRefresh={
source === "sqlite" ? loadEventsFromDB : loadEventsFromDevice
}
/>
<Modal
visible={modalVisible}
animationType="slide"
transparent={true}
onRequestClose={() => setModalVisible(false)}
>
<View style={styles.modalOverlay}>
<View style={styles.modalContent}>
<Text style={styles.header}>Neuer SQLite-Eintrag</Text>
<TextInput
style={styles.input}
placeholder="Titel"
value={newTitle}
onChangeText={setNewTitle}
/>
<TextInput
style={styles.input}
placeholder="Start (YYYY-MM-DD HH:mm)"
value={newStart}
onChangeText={setNewStart}
/>
<TextInput
style={styles.input}
placeholder="Ende (YYYY-MM-DD HH:mm)"
value={newEnd}
onChangeText={setNewEnd}
/>
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
marginTop: 10,
}}
>
<Button
title="Abbrechen"
onPress={() => setModalVisible(false)}
/>
<Button title="Speichern" onPress={addEventToDB} />
</View>
</View>
</View>
</Modal>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: "#fff", padding: 10 },
header: {
fontSize: 20,
fontWeight: "bold",
marginBottom: 16,
textAlign: "center",
},
item: {
marginBottom: 12,
padding: 12,
borderRadius: 8,
backgroundColor: "#f2f2f2",
},
title: { fontSize: 16, fontWeight: "600" },
time: { fontSize: 14, color: "#555" },
switchRow: {
flexDirection: "row",
justifyContent: "center",
marginBottom: 16,
},
switchButton: {
padding: 10,
borderRadius: 8,
backgroundColor: "#eee",
marginHorizontal: 8,
},
activeButton: { backgroundColor: "#007AFF" },
switchText: { color: "#333", fontWeight: "bold" },
modalOverlay: {
flex: 1,
backgroundColor: "rgba(0,0,0,0.2)",
justifyContent: "center",
alignItems: "center",
},
modalContent: {
width: 320,
backgroundColor: "#fff",
borderRadius: 12,
padding: 20,
elevation: 8,
},
input: {
borderWidth: 1,
borderColor: "#ccc",
borderRadius: 8,
padding: 10,
marginBottom: 12,
fontSize: 16,
},
});