138 lines
5.6 KiB
TypeScript
138 lines
5.6 KiB
TypeScript
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 },
|
|
});
|