195 lines
8.3 KiB
TypeScript
195 lines
8.3 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 { EventCreateModal } from '@/components/event-create-modal';
|
|
import { HistorySection } from '@/components/history-section';
|
|
import { CalendarEvent, EventState, fetchEventById, fetchProjects, Project, updateEvent } 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 [projects, setProjects] = useState<Project[]>([]);
|
|
const [editOpen, setEditOpen] = useState(false);
|
|
const [updatingState, setUpdatingState] = useState(false);
|
|
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 {
|
|
const [eventRow, projectRows] = await Promise.all([
|
|
fetchEventById(token, eventId),
|
|
fetchProjects(token),
|
|
]);
|
|
setEvent(eventRow);
|
|
setProjects(projectRows);
|
|
} 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 });
|
|
}
|
|
|
|
async function changeState(nextState: EventState) {
|
|
if (!token || !event || updatingState || event.state === nextState) return;
|
|
setUpdatingState(true);
|
|
setError(null);
|
|
try {
|
|
const updated = await updateEvent(token, eventId, { state: nextState });
|
|
setEvent((current) => current ? { ...current, state: updated.state } : updated);
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Der Status konnte nicht geändert werden.');
|
|
} finally {
|
|
setUpdatingState(false);
|
|
}
|
|
}
|
|
|
|
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: '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>
|
|
<Pressable style={styles.editButton} onPress={() => setEditOpen(true)}>
|
|
<Text style={styles.editButtonText}>Bearbeiten</Text>
|
|
</Pressable>
|
|
</View>
|
|
<View style={styles.statusSection}>
|
|
<Text style={styles.statusLabel}>Status</Text>
|
|
<View style={styles.statusRow}>
|
|
{(['Entwurf', 'Final'] as EventState[]).map((option) => (
|
|
<Pressable
|
|
key={option}
|
|
disabled={updatingState}
|
|
style={[styles.statusButton, event.state === option ? styles.statusButtonActive : null]}
|
|
onPress={() => changeState(option)}>
|
|
<Text style={[styles.statusButtonText, event.state === option ? styles.statusButtonTextActive : null]}>
|
|
{option}
|
|
</Text>
|
|
</Pressable>
|
|
))}
|
|
</View>
|
|
</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} />
|
|
<EventCreateModal
|
|
visible={editOpen}
|
|
event={event}
|
|
projects={projects}
|
|
onClose={() => setEditOpen(false)}
|
|
onUpdated={() => load(false)}
|
|
/>
|
|
</>
|
|
) : 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' },
|
|
editButton: { borderRadius: 8, backgroundColor: PRIMARY, paddingHorizontal: 10, paddingVertical: 7 },
|
|
editButtonText: { color: '#ffffff', fontSize: 12, fontWeight: '700' },
|
|
statusSection: { gap: 6 },
|
|
statusLabel: { color: '#6b7280', fontSize: 12, textTransform: 'uppercase', fontWeight: '600' },
|
|
statusRow: { flexDirection: 'row', gap: 8 },
|
|
statusButton: { flex: 1, minHeight: 40, borderRadius: 9, borderWidth: 1, borderColor: '#d1d5db', alignItems: 'center', justifyContent: 'center' },
|
|
statusButtonActive: { borderColor: PRIMARY, backgroundColor: '#eff9ea' },
|
|
statusButtonText: { color: '#6b7280', fontSize: 13, fontWeight: '700' },
|
|
statusButtonTextActive: { color: '#3d7a30' },
|
|
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 },
|
|
});
|