Files
FEDEO/mobile/app/more/events.tsx

209 lines
8.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 },
});