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

View File

@@ -0,0 +1,137 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { ActivityIndicator, Pressable, RefreshControl, ScrollView, StyleSheet, Text, View } from 'react-native';
import { useLocalSearchParams } from 'expo-router';
import * as WebBrowser from 'expo-web-browser';
import { HistorySection } from '@/components/history-section';
import { CalendarEvent, fetchEventById } from '@/src/lib/api';
import { useAuth } from '@/src/providers/auth-provider';
const PRIMARY = '#69c350';
function formatDateTime(value: unknown): string {
if (!value) return '-';
const date = new Date(String(value));
if (Number.isNaN(date.getTime())) return String(value);
return date.toLocaleString('de-DE', {
weekday: 'short',
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
}
function relationName(value: unknown): string {
if (!value) return '-';
if (typeof value === 'object') {
const relation = value as { id?: number | string; name?: string };
return String(relation.name || relation.id || '-');
}
return String(value);
}
export default function EventDetailScreen() {
const params = useLocalSearchParams<{ id?: string }>();
const eventId = Number(params.id);
const { token } = useAuth();
const [event, setEvent] = useState<CalendarEvent | null>(null);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [error, setError] = useState<string | null>(null);
const validId = useMemo(() => Number.isFinite(eventId) && eventId > 0, [eventId]);
const load = useCallback(async (showSpinner = true) => {
if (!token || !validId) return;
if (showSpinner) setLoading(true);
setError(null);
try {
setEvent(await fetchEventById(token, eventId));
} catch (err) {
setError(err instanceof Error ? err.message : 'Der Termin konnte nicht geladen werden.');
} finally {
setLoading(false);
setRefreshing(false);
}
}, [eventId, token, validId]);
useEffect(() => {
void load(true);
}, [load]);
async function onRefresh() {
setRefreshing(true);
await load(false);
}
async function openLink() {
if (!event?.link) return;
const raw = String(event.link).trim();
const url = /^https?:\/\//i.test(raw) ? raw : `https://${raw}`;
await WebBrowser.openBrowserAsync(url, { controlsColor: PRIMARY, showTitle: true });
}
const rows = event
? [
{ label: 'Beginn', value: formatDateTime(event.startDate) },
{ label: 'Ende', value: formatDateTime(event.endDate) },
{ label: 'Projekt', value: relationName(event.project) },
{ label: 'Kunde', value: relationName(event.customer) },
{ label: 'Status', value: String(event.state || '-') },
{ label: 'Wiederholung', value: String(event.repeatInterval || '-') },
]
: [];
return (
<ScrollView
contentContainerStyle={styles.container}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}>
{error ? <Text style={styles.error}>{error}</Text> : null}
{loading ? (
<View style={styles.loadingBox}><ActivityIndicator /><Text style={styles.loadingText}>Termin wird geladen...</Text></View>
) : null}
{!loading && event ? (
<>
<View style={styles.card}>
<View style={styles.headingRow}>
<View style={[styles.colorMark, { backgroundColor: event.color || PRIMARY }]} />
<Text style={styles.title}>{event.name}</Text>
</View>
<View style={styles.table}>
{rows.map((row) => (
<View key={row.label} style={styles.row}>
<Text style={styles.label}>{row.label}</Text>
<Text style={styles.value}>{row.value}</Text>
</View>
))}
</View>
{event.notes ? <View style={styles.notesBox}><Text style={styles.notesLabel}>Notizen</Text><Text style={styles.notes}>{event.notes}</Text></View> : null}
{event.link ? <Pressable style={styles.linkButton} onPress={openLink}><Text style={styles.linkButtonText}>Link öffnen</Text></Pressable> : null}
</View>
<HistorySection resource="events" resourceId={eventId} />
</>
) : null}
</ScrollView>
);
}
const styles = StyleSheet.create({
container: { padding: 16, gap: 12, backgroundColor: '#f9fafb' },
card: { backgroundColor: '#ffffff', borderRadius: 12, borderWidth: 1, borderColor: '#e5e7eb', padding: 12, gap: 10 },
headingRow: { flexDirection: 'row', alignItems: 'center', gap: 9 },
colorMark: { width: 6, height: 28, borderRadius: 3 },
title: { flex: 1, color: '#111827', fontSize: 19, fontWeight: '700' },
table: { borderWidth: 1, borderColor: '#e5e7eb', borderRadius: 10, overflow: 'hidden' },
row: { borderBottomWidth: 1, borderBottomColor: '#e5e7eb', paddingHorizontal: 10, paddingVertical: 8, gap: 2 },
label: { color: '#6b7280', fontSize: 12, textTransform: 'uppercase', fontWeight: '600' },
value: { color: '#111827', fontSize: 14, fontWeight: '500' },
notesBox: { borderWidth: 1, borderColor: '#e5e7eb', borderRadius: 10, padding: 10, backgroundColor: '#fafafa', gap: 4 },
notesLabel: { color: '#6b7280', fontSize: 12, fontWeight: '600', textTransform: 'uppercase' },
notes: { color: '#374151', fontSize: 14 },
linkButton: { backgroundColor: PRIMARY, borderRadius: 9, minHeight: 40, alignItems: 'center', justifyContent: 'center' },
linkButtonText: { color: '#ffffff', fontWeight: '700' },
loadingBox: { paddingVertical: 24, alignItems: 'center', gap: 8 },
loadingText: { color: '#6b7280' },
error: { color: '#dc2626', fontSize: 13 },
});