454 lines
13 KiB
TypeScript
454 lines
13 KiB
TypeScript
import {
|
||
View, Text, ScrollView, TouchableOpacity, Linking, ActivityIndicator, Alert, Share, StyleSheet, Platform,
|
||
} from 'react-native'
|
||
import { SafeAreaView } from 'react-native-safe-area-context'
|
||
import { useLocalSearchParams, useRouter, Stack } from 'expo-router'
|
||
import { Ionicons } from '@expo/vector-icons'
|
||
import { useTerminDetail, useToggleAnmeldung } from '@/hooks/useTermine'
|
||
import { AnmeldeButton } from '@/components/termine/AnmeldeButton'
|
||
import { Badge } from '@/components/ui/Badge'
|
||
import { TERMIN_TYP_LABELS } from '@innungsapp/shared/types'
|
||
import { format } from 'date-fns'
|
||
import { de } from 'date-fns/locale'
|
||
import * as Calendar from 'expo-calendar'
|
||
|
||
export default function TerminDetailScreen() {
|
||
const { id } = useLocalSearchParams<{ id: string }>()
|
||
const router = useRouter()
|
||
const { data: termin, isLoading } = useTerminDetail(id)
|
||
const { mutate, isPending } = useToggleAnmeldung()
|
||
|
||
if (isLoading) {
|
||
return (
|
||
<View style={styles.loadingContainer}>
|
||
<ActivityIndicator size="large" color="#003B7E" />
|
||
</View>
|
||
)
|
||
}
|
||
if (!termin) return null
|
||
|
||
const datum = new Date(termin.datum)
|
||
const isPast = datum < new Date()
|
||
|
||
const handleAddToCalendar = async () => {
|
||
try {
|
||
const { status } = await Calendar.requestCalendarPermissionsAsync()
|
||
if (status !== 'granted') {
|
||
Alert.alert('Fehler', 'Kalender-Berechtigung wurde verweigert.')
|
||
return
|
||
}
|
||
|
||
const startDate = new Date(termin.datum)
|
||
let endDate = new Date(termin.datum)
|
||
|
||
if (termin.uhrzeit) {
|
||
const [hours, minutes] = termin.uhrzeit.split(':').map(Number)
|
||
startDate.setHours(hours || 0, minutes || 0)
|
||
|
||
if (termin.endeUhrzeit) {
|
||
const [endHours, endMinutes] = termin.endeUhrzeit.split(':').map(Number)
|
||
endDate.setHours(endHours || 0, endMinutes || 0)
|
||
} else {
|
||
endDate.setHours((hours || 0) + 1, minutes || 0)
|
||
}
|
||
} else {
|
||
endDate.setDate(startDate.getDate() + 1)
|
||
}
|
||
|
||
let calendarId
|
||
if (Platform.OS === 'ios') {
|
||
const defaultCalendar = await Calendar.getDefaultCalendarAsync()
|
||
calendarId = defaultCalendar.id
|
||
} else {
|
||
const calendars = await Calendar.getCalendarsAsync(Calendar.EntityTypes.EVENT)
|
||
// Try to prefer primary or typical default calendar
|
||
const primary = calendars.find(c => c.isPrimary) || calendars.find(c => c.accessLevel === 'owner') || calendars[0]
|
||
calendarId = primary?.id
|
||
}
|
||
|
||
if (!calendarId) {
|
||
Alert.alert('Fehler', 'Kein beschreibbarer Kalender gefunden.')
|
||
return
|
||
}
|
||
|
||
await Calendar.createEventAsync(calendarId, {
|
||
title: termin.titel,
|
||
startDate,
|
||
endDate,
|
||
allDay: !termin.uhrzeit,
|
||
location: [termin.ort, termin.adresse].filter(Boolean).join(', '),
|
||
notes: termin.beschreibung || undefined,
|
||
})
|
||
|
||
Alert.alert('Erfolg', 'Der Termin wurde in den Kalender eingetragen.')
|
||
} catch (e) {
|
||
console.error(e)
|
||
Alert.alert('Fehler', 'Der Termin konnte nicht eingetragen werden.')
|
||
}
|
||
}
|
||
|
||
return (
|
||
<>
|
||
<Stack.Screen options={{ headerShown: false }} />
|
||
<SafeAreaView style={styles.container} edges={['top']}>
|
||
{/* Header */}
|
||
<View style={styles.header}>
|
||
<TouchableOpacity onPress={() => router.back()} style={styles.backButton}>
|
||
<Ionicons name="arrow-back" size={24} color="#0F172A" />
|
||
</TouchableOpacity>
|
||
<View style={styles.headerSpacer} />
|
||
<TouchableOpacity
|
||
style={styles.shareButton}
|
||
onPress={() => {
|
||
const lines: string[] = []
|
||
lines.push(`📅 ${termin.titel}`)
|
||
lines.push(format(datum, 'EEEE, d. MMMM yyyy', { locale: de }))
|
||
if (termin.uhrzeit) {
|
||
lines.push(`🕐 ${termin.uhrzeit}${termin.endeUhrzeit ? ` – ${termin.endeUhrzeit}` : ''} Uhr`)
|
||
}
|
||
if (termin.ort) lines.push(`📍 ${termin.ort}`)
|
||
if (termin.adresse) lines.push(` ${termin.adresse}`)
|
||
if (termin.beschreibung) lines.push(`\n${termin.beschreibung}`)
|
||
Share.share({ message: lines.join('\n') })
|
||
}}
|
||
>
|
||
<Ionicons name="share-outline" size={24} color="#0F172A" />
|
||
</TouchableOpacity>
|
||
</View>
|
||
|
||
<ScrollView contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false}>
|
||
{/* Date Badge */}
|
||
<View style={styles.dateline}>
|
||
<View style={styles.calendarIcon}>
|
||
<Ionicons name="calendar" size={18} color="#003B7E" />
|
||
</View>
|
||
<Text style={styles.dateText}>
|
||
{format(datum, 'EEEE, d. MMMM yyyy', { locale: de })}
|
||
</Text>
|
||
</View>
|
||
|
||
<Text style={styles.title}>{termin.titel}</Text>
|
||
|
||
<View style={styles.badgesRow}>
|
||
<Badge label={TERMIN_TYP_LABELS[termin.typ]} typ={termin.typ} />
|
||
{termin.isAngemeldet && (
|
||
<View style={styles.registeredBadge}>
|
||
<Ionicons name="checkmark-circle" size={14} color="#059669" />
|
||
<Text style={styles.registeredText}>Angemeldet</Text>
|
||
</View>
|
||
)}
|
||
</View>
|
||
|
||
{/* Info Card */}
|
||
<View style={styles.card}>
|
||
{/* Time */}
|
||
{termin.uhrzeit && (
|
||
<View style={styles.infoRow}>
|
||
<View style={styles.iconBox}>
|
||
<Ionicons name="time-outline" size={22} color="#64748B" />
|
||
</View>
|
||
<View style={styles.infoContent}>
|
||
<Text style={styles.infoLabel}>Uhrzeit</Text>
|
||
<Text style={styles.infoValue}>
|
||
{termin.uhrzeit}
|
||
{termin.endeUhrzeit ? ` – ${termin.endeUhrzeit}` : ''} Uhr
|
||
</Text>
|
||
</View>
|
||
</View>
|
||
)}
|
||
|
||
{termin.uhrzeit && termin.ort && <View style={styles.divider} />}
|
||
|
||
{/* Location */}
|
||
{termin.ort && (
|
||
<TouchableOpacity
|
||
style={styles.infoRow}
|
||
onPress={() =>
|
||
termin.adresse &&
|
||
Linking.openURL(`https://maps.google.com/?q=${encodeURIComponent(termin.adresse)}`)
|
||
}
|
||
activeOpacity={termin.adresse ? 0.7 : 1}
|
||
>
|
||
<View style={styles.iconBox}>
|
||
<Ionicons name="location-outline" size={22} color="#64748B" />
|
||
</View>
|
||
<View style={styles.infoContent}>
|
||
<Text style={styles.infoLabel}>Ort</Text>
|
||
<Text style={styles.infoValue}>{termin.ort}</Text>
|
||
{termin.adresse && (
|
||
<Text style={styles.infoSub}>{termin.adresse}</Text>
|
||
)}
|
||
</View>
|
||
{termin.adresse && (
|
||
<Ionicons name="chevron-forward" size={20} color="#CBD5E1" />
|
||
)}
|
||
</TouchableOpacity>
|
||
)}
|
||
|
||
{(termin.uhrzeit || termin.ort) && (termin.teilnehmerAnzahl !== undefined) && <View style={styles.divider} />}
|
||
|
||
{/* Participants */}
|
||
<View style={styles.infoRow}>
|
||
<View style={styles.iconBox}>
|
||
<Ionicons name="people-outline" size={22} color="#64748B" />
|
||
</View>
|
||
<View style={styles.infoContent}>
|
||
<Text style={styles.infoLabel}>Teilnehmer</Text>
|
||
<Text style={styles.infoValue}>
|
||
{termin.teilnehmerAnzahl} Anmeldungen
|
||
</Text>
|
||
{termin.maxTeilnehmer && (
|
||
<Text style={styles.infoSub}>{termin.maxTeilnehmer} Plätze verfügbar</Text>
|
||
)}
|
||
</View>
|
||
</View>
|
||
</View>
|
||
|
||
{/* Description */}
|
||
{termin.beschreibung && (
|
||
<View style={styles.section}>
|
||
<Text style={styles.sectionTitle}>Beschreibung</Text>
|
||
<Text style={styles.description}>{termin.beschreibung}</Text>
|
||
</View>
|
||
)}
|
||
|
||
</ScrollView>
|
||
|
||
{/* Bottom Action Bar – only for upcoming events */}
|
||
{isPast ? (
|
||
<View style={styles.pastBar}>
|
||
<Ionicons name="time-outline" size={16} color="#94A3B8" />
|
||
<Text style={styles.pastBarText}>Vergangener Termin – keine Anmeldung möglich</Text>
|
||
</View>
|
||
) : (
|
||
<View style={styles.actionBar}>
|
||
<View style={styles.actionButtonContainer}>
|
||
<AnmeldeButton
|
||
terminId={id}
|
||
isAngemeldet={termin.isAngemeldet}
|
||
onToggle={() => mutate({ terminId: id })}
|
||
isLoading={isPending}
|
||
maxTeilnehmer={termin.maxTeilnehmer}
|
||
teilnehmerAnzahl={termin.teilnehmerAnzahl}
|
||
/>
|
||
</View>
|
||
<TouchableOpacity
|
||
style={styles.calendarButton}
|
||
onPress={handleAddToCalendar}
|
||
>
|
||
<Ionicons name="calendar-outline" size={24} color="#0F172A" />
|
||
</TouchableOpacity>
|
||
</View>
|
||
)}
|
||
|
||
</SafeAreaView>
|
||
</>
|
||
)
|
||
}
|
||
|
||
const styles = StyleSheet.create({
|
||
container: {
|
||
flex: 1,
|
||
backgroundColor: '#F8FAFC',
|
||
},
|
||
loadingContainer: {
|
||
flex: 1,
|
||
justifyContent: 'center',
|
||
alignItems: 'center',
|
||
},
|
||
header: {
|
||
paddingHorizontal: 16,
|
||
paddingVertical: 12,
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
backgroundColor: '#F8FAFC',
|
||
},
|
||
backButton: {
|
||
padding: 8,
|
||
borderRadius: 12,
|
||
backgroundColor: '#FFFFFF',
|
||
shadowColor: '#000',
|
||
shadowOffset: { width: 0, height: 1 },
|
||
shadowOpacity: 0.05,
|
||
shadowRadius: 2,
|
||
elevation: 1,
|
||
},
|
||
headerSpacer: {
|
||
flex: 1,
|
||
},
|
||
shareButton: {
|
||
padding: 8,
|
||
},
|
||
scrollContent: {
|
||
padding: 24,
|
||
paddingBottom: 100,
|
||
},
|
||
dateline: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
marginBottom: 12,
|
||
gap: 8,
|
||
},
|
||
calendarIcon: {
|
||
width: 32,
|
||
height: 32,
|
||
borderRadius: 8,
|
||
backgroundColor: '#EFF6FF',
|
||
justifyContent: 'center',
|
||
alignItems: 'center',
|
||
},
|
||
dateText: {
|
||
fontSize: 15,
|
||
fontWeight: '600',
|
||
color: '#003B7E',
|
||
},
|
||
title: {
|
||
fontSize: 28,
|
||
fontWeight: '800',
|
||
color: '#0F172A',
|
||
lineHeight: 34,
|
||
marginBottom: 16,
|
||
letterSpacing: -0.5,
|
||
},
|
||
badgesRow: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
gap: 12,
|
||
marginBottom: 32,
|
||
},
|
||
registeredBadge: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
gap: 6,
|
||
paddingHorizontal: 10,
|
||
paddingVertical: 4,
|
||
backgroundColor: '#ECFDF5',
|
||
borderRadius: 6,
|
||
borderWidth: 1,
|
||
borderColor: '#A7F3D0',
|
||
},
|
||
registeredText: {
|
||
fontSize: 12,
|
||
fontWeight: '600',
|
||
color: '#047857',
|
||
},
|
||
card: {
|
||
backgroundColor: '#FFFFFF',
|
||
borderRadius: 20,
|
||
padding: 20,
|
||
shadowColor: '#64748B',
|
||
shadowOffset: { width: 0, height: 4 },
|
||
shadowOpacity: 0.04,
|
||
shadowRadius: 12,
|
||
elevation: 2,
|
||
marginBottom: 32,
|
||
},
|
||
infoRow: {
|
||
flexDirection: 'row',
|
||
alignItems: 'flex-start',
|
||
gap: 16,
|
||
},
|
||
iconBox: {
|
||
width: 40,
|
||
height: 40,
|
||
borderRadius: 10,
|
||
backgroundColor: '#F1F5F9',
|
||
justifyContent: 'center',
|
||
alignItems: 'center',
|
||
},
|
||
infoContent: {
|
||
flex: 1,
|
||
paddingTop: 2,
|
||
},
|
||
infoLabel: {
|
||
fontSize: 12,
|
||
fontWeight: '600',
|
||
color: '#94A3B8',
|
||
marginBottom: 2,
|
||
textTransform: 'uppercase',
|
||
letterSpacing: 0.5,
|
||
},
|
||
infoValue: {
|
||
fontSize: 16,
|
||
fontWeight: '600',
|
||
color: '#0F172A',
|
||
lineHeight: 22,
|
||
},
|
||
infoSub: {
|
||
fontSize: 14,
|
||
color: '#64748B',
|
||
marginTop: 2,
|
||
lineHeight: 20,
|
||
},
|
||
divider: {
|
||
height: 1,
|
||
backgroundColor: '#F1F5F9',
|
||
marginVertical: 16,
|
||
marginLeft: 56,
|
||
},
|
||
section: {
|
||
marginBottom: 24,
|
||
},
|
||
sectionTitle: {
|
||
fontSize: 18,
|
||
fontWeight: '700',
|
||
color: '#0F172A',
|
||
marginBottom: 12,
|
||
},
|
||
description: {
|
||
fontSize: 16,
|
||
lineHeight: 26,
|
||
color: '#334155',
|
||
},
|
||
actionBar: {
|
||
position: 'absolute',
|
||
bottom: 0,
|
||
left: 0,
|
||
right: 0,
|
||
backgroundColor: '#FFFFFF',
|
||
paddingHorizontal: 20,
|
||
paddingVertical: 16,
|
||
paddingBottom: Platform.OS === 'ios' ? 32 : 16,
|
||
flexDirection: 'row',
|
||
gap: 12,
|
||
borderTopWidth: 1,
|
||
borderTopColor: '#F1F5F9',
|
||
shadowColor: '#000',
|
||
shadowOffset: { width: 0, height: -4 },
|
||
shadowOpacity: 0.02,
|
||
shadowRadius: 8,
|
||
elevation: 4,
|
||
},
|
||
pastBar: {
|
||
position: 'absolute',
|
||
bottom: 0,
|
||
left: 0,
|
||
right: 0,
|
||
backgroundColor: '#F8FAFC',
|
||
paddingHorizontal: 20,
|
||
paddingVertical: 14,
|
||
paddingBottom: Platform.OS === 'ios' ? 28 : 14,
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
gap: 8,
|
||
borderTopWidth: 1,
|
||
borderTopColor: '#E2E8F0',
|
||
},
|
||
pastBarText: {
|
||
fontSize: 13,
|
||
color: '#94A3B8',
|
||
fontWeight: '500',
|
||
},
|
||
actionButtonContainer: {
|
||
flex: 1,
|
||
},
|
||
calendarButton: {
|
||
width: 52,
|
||
height: 52,
|
||
borderRadius: 14,
|
||
backgroundColor: '#F1F5F9',
|
||
justifyContent: 'center',
|
||
alignItems: 'center',
|
||
},
|
||
})
|