KI-AGENT: Mobile Termine und Kalender ergänzen
This commit is contained in:
208
mobile/app/more/events.tsx
Normal file
208
mobile/app/more/events.tsx
Normal 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 },
|
||||
});
|
||||
Reference in New Issue
Block a user