stadtwerke/innungsapp/apps/mobile/app/(app)/termine/[id].tsx

380 lines
10 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 {
View, Text, ScrollView, TouchableOpacity, Linking, ActivityIndicator, Alert, 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'
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()
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}>
<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}
/>
</View>
<TouchableOpacity
style={styles.calendarButton}
onPress={() => Alert.alert('Kalender', 'Funktion folgt in Kürze')}
>
<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',
},
})