KI-AGENT: Mobile Termine und Kalender ergänzen

This commit is contained in:
2026-09-08 22:05:18 +02:00
parent 5e1631e2d3
commit 92c354c1b9
8 changed files with 1122 additions and 1 deletions

208
mobile/app/more/events.tsx Normal file
View File

@@ -0,0 +1,208 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import {
ActivityIndicator,
Pressable,
RefreshControl,
ScrollView,
StyleSheet,
Text,
TextInput,
View,
} from 'react-native';
import { router } from 'expo-router';
import { EventCreateModal } from '@/components/event-create-modal';
import { CalendarEvent, fetchEvents, fetchProjects, Project } from '@/src/lib/api';
import { useAuth } from '@/src/providers/auth-provider';
const PRIMARY = '#69c350';
function formatEventDate(event: CalendarEvent): string {
const start = new Date(event.startDate);
const end = event.endDate ? new Date(event.endDate) : null;
if (Number.isNaN(start.getTime())) return String(event.startDate || '-');
const date = start.toLocaleDateString('de-DE', { weekday: 'short', day: '2-digit', month: '2-digit', year: 'numeric' });
const startTime = start.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
const endTime = end && !Number.isNaN(end.getTime())
? end.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })
: null;
return `${date} · ${startTime}${endTime ? `${endTime}` : ''}`;
}
function projectName(event: CalendarEvent): string | null {
if (!event.project) return null;
if (typeof event.project === 'object') return event.project.name || (event.project.id ? `Projekt #${event.project.id}` : null);
return `Projekt #${event.project}`;
}
export default function EventsScreen() {
const { token } = useAuth();
const [events, setEvents] = useState<CalendarEvent[]>([]);
const [projects, setProjects] = useState<Project[]>([]);
const [search, setSearch] = useState('');
const [showPast, setShowPast] = useState(false);
const [createOpen, setCreateOpen] = useState(false);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [error, setError] = useState<string | null>(null);
const load = useCallback(async (showSpinner = true) => {
if (!token) return;
if (showSpinner) setLoading(true);
setError(null);
try {
const [eventRows, projectRows] = await Promise.all([fetchEvents(token), fetchProjects(token)]);
setEvents(eventRows);
setProjects(projectRows);
} catch (err) {
setError(err instanceof Error ? err.message : 'Termine konnten nicht geladen werden.');
} finally {
setLoading(false);
setRefreshing(false);
}
}, [token]);
useEffect(() => {
void load(true);
}, [load]);
const visibleEvents = useMemo(() => {
const now = Date.now();
const terms = search.trim().toLowerCase().split(/\s+/).filter(Boolean);
return events
.filter((event) => showPast || new Date(event.endDate || event.startDate).getTime() >= now)
.filter((event) => {
if (terms.length === 0) return true;
const haystack = [event.name, event.notes, event.eventtype, projectName(event)]
.map((value) => String(value || '').toLowerCase())
.join(' ');
return terms.every((term) => haystack.includes(term));
})
.sort((a, b) => new Date(a.startDate).getTime() - new Date(b.startDate).getTime());
}, [events, search, showPast]);
async function onRefresh() {
setRefreshing(true);
await load(false);
}
return (
<View style={styles.screen}>
<View style={styles.toolbar}>
<TextInput
placeholder="Termine suchen"
placeholderTextColor="#9ca3af"
style={styles.searchInput}
value={search}
onChangeText={setSearch}
/>
<View style={styles.toolbarActions}>
<Pressable
style={[styles.filterButton, showPast ? styles.filterButtonActive : null]}
onPress={() => setShowPast((value) => !value)}>
<Text style={[styles.filterButtonText, showPast ? styles.filterButtonTextActive : null]}>Vergangene anzeigen</Text>
</Pressable>
<Pressable style={styles.calendarButton} onPress={() => router.push('/more/calendar' as any)}>
<Text style={styles.calendarButtonText}>Kalender</Text>
</Pressable>
</View>
</View>
<ScrollView
style={styles.list}
contentContainerStyle={styles.listContent}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}>
{error ? <Text style={styles.error}>{error}</Text> : null}
{loading ? (
<View style={styles.loadingBox}>
<ActivityIndicator />
<Text style={styles.loadingText}>Termine werden geladen...</Text>
</View>
) : null}
{!loading && visibleEvents.length === 0 ? <Text style={styles.empty}>Keine Termine gefunden.</Text> : null}
{!loading
? visibleEvents.map((event) => (
<Pressable
key={String(event.id)}
style={({ pressed }) => [styles.eventRow, pressed ? styles.eventRowPressed : null]}
onPress={() => router.push(`/more/event/${event.id}` as any)}>
<View style={[styles.colorBar, { backgroundColor: event.color || PRIMARY }]} />
<View style={styles.eventMain}>
<Text style={styles.eventName}>{event.name}</Text>
<Text style={styles.eventDate}>{formatEventDate(event)}</Text>
{projectName(event) ? <Text style={styles.eventProject}>{projectName(event)}</Text> : null}
{event.notes ? <Text style={styles.eventNotes} numberOfLines={2}>{String(event.notes)}</Text> : null}
</View>
<Text style={styles.arrow}></Text>
</Pressable>
))
: null}
</ScrollView>
<Pressable style={styles.fab} onPress={() => setCreateOpen(true)}>
<Text style={styles.fabText}>+</Text>
</Pressable>
<EventCreateModal
visible={createOpen}
projects={projects}
onClose={() => setCreateOpen(false)}
onCreated={() => load(false)}
/>
</View>
);
}
const styles = StyleSheet.create({
screen: { flex: 1, backgroundColor: '#ffffff' },
toolbar: { padding: 14, gap: 10, borderBottomWidth: 1, borderBottomColor: '#e5e7eb' },
searchInput: {
borderWidth: 1,
borderColor: '#d1d5db',
borderRadius: 10,
paddingHorizontal: 12,
paddingVertical: 10,
fontSize: 15,
color: '#111827',
},
toolbarActions: { flexDirection: 'row', justifyContent: 'space-between', gap: 8 },
filterButton: { borderWidth: 1, borderColor: '#d1d5db', borderRadius: 999, paddingHorizontal: 10, paddingVertical: 7 },
filterButtonActive: { borderColor: PRIMARY, backgroundColor: '#eff9ea' },
filterButtonText: { color: '#374151', fontSize: 12, fontWeight: '600' },
filterButtonTextActive: { color: '#3d7a30' },
calendarButton: { borderRadius: 8, backgroundColor: PRIMARY, paddingHorizontal: 13, paddingVertical: 7 },
calendarButtonText: { color: '#ffffff', fontSize: 13, fontWeight: '700' },
list: { flex: 1 },
listContent: { paddingBottom: 96 },
eventRow: { minHeight: 82, flexDirection: 'row', alignItems: 'stretch', borderBottomWidth: 1, borderBottomColor: '#e5e7eb' },
eventRowPressed: { backgroundColor: '#f3f4f6' },
colorBar: { width: 5 },
eventMain: { flex: 1, paddingHorizontal: 12, paddingVertical: 11, gap: 2 },
eventName: { color: '#111827', fontSize: 15, fontWeight: '700' },
eventDate: { color: '#374151', fontSize: 13 },
eventProject: { color: '#3d7a30', fontSize: 12, fontWeight: '600' },
eventNotes: { color: '#6b7280', fontSize: 12 },
arrow: { alignSelf: 'center', color: '#9ca3af', fontSize: 25, paddingRight: 12 },
loadingBox: { paddingVertical: 24, alignItems: 'center', gap: 8 },
loadingText: { color: '#6b7280' },
empty: { color: '#6b7280', textAlign: 'center', paddingVertical: 28 },
error: { color: '#dc2626', fontSize: 13, padding: 14 },
fab: {
position: 'absolute',
right: 18,
bottom: 20,
width: 56,
height: 56,
borderRadius: 28,
backgroundColor: PRIMARY,
alignItems: 'center',
justifyContent: 'center',
shadowColor: '#111827',
shadowOpacity: 0.25,
shadowRadius: 8,
shadowOffset: { width: 0, height: 4 },
elevation: 5,
},
fabText: { color: '#ffffff', fontSize: 30, lineHeight: 30, marginTop: -2 },
});