299 lines
9.5 KiB
TypeScript
299 lines
9.5 KiB
TypeScript
import { Tabs, Redirect } from 'expo-router'
|
|
import { Platform, View, Text, StyleSheet, TextInput, TouchableOpacity, ActivityIndicator, Alert, ScrollView } from 'react-native'
|
|
import { useEffect, useState } from 'react'
|
|
import { Ionicons } from '@expo/vector-icons'
|
|
import { SafeAreaView } from 'react-native-safe-area-context'
|
|
import { useAuthStore } from '@/store/auth.store'
|
|
import { trpc } from '@/lib/trpc'
|
|
import { setupPushNotifications } from '@/lib/notifications'
|
|
import { authClient } from '@/lib/auth-client'
|
|
|
|
function UnreadBadge({ count }: { count: number }) {
|
|
if (count === 0) return null
|
|
return (
|
|
<View style={badge.dot}>
|
|
<Text style={badge.text}>{count > 9 ? '9+' : count}</Text>
|
|
</View>
|
|
)
|
|
}
|
|
|
|
const badge = StyleSheet.create({
|
|
dot: {
|
|
position: 'absolute',
|
|
top: -4,
|
|
right: -8,
|
|
minWidth: 17,
|
|
height: 17,
|
|
borderRadius: 9,
|
|
backgroundColor: '#DC2626',
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
paddingHorizontal: 4,
|
|
borderWidth: 2,
|
|
borderColor: '#FFFFFF',
|
|
},
|
|
text: {
|
|
fontSize: 10,
|
|
fontWeight: '700',
|
|
color: '#FFFFFF',
|
|
lineHeight: 13,
|
|
},
|
|
})
|
|
|
|
function ChatTabIcon({ color, focused }: { color: string; focused: boolean }) {
|
|
const { data: unreadCount } = trpc.messages.getConversations.useQuery(undefined, {
|
|
refetchInterval: 15_000,
|
|
staleTime: 10_000,
|
|
select: (data) => data.filter((c) => c.hasUnread).length,
|
|
})
|
|
|
|
return (
|
|
<View>
|
|
<Ionicons name={focused ? 'chatbubbles' : 'chatbubbles-outline'} size={23} color={color} />
|
|
<UnreadBadge count={unreadCount ?? 0} />
|
|
</View>
|
|
)
|
|
}
|
|
|
|
function ForcePasswordChangeScreen() {
|
|
const { setSession, signOut } = useAuthStore()
|
|
const [next, setNext] = useState('')
|
|
const [confirm, setConfirm] = useState('')
|
|
const [loading, setLoading] = useState(false)
|
|
const [error, setError] = useState('')
|
|
|
|
async function handleSubmit() {
|
|
setError('')
|
|
if (next.length < 8) { setError('Das neue Passwort muss mindestens 8 Zeichen haben.'); return }
|
|
if (next !== confirm) { setError('Die Passwörter stimmen nicht überein.'); return }
|
|
|
|
setLoading(true)
|
|
// Set password directly via tRPC (no old password needed — user is already authenticated)
|
|
try {
|
|
const apiUrl = process.env.EXPO_PUBLIC_API_URL ?? 'http://localhost:3032'
|
|
const sessionResult = await authClient.getSession()
|
|
const token = (sessionResult?.data as any)?.session?.token
|
|
const res = await fetch(`${apiUrl}/api/auth/force-set-password`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
},
|
|
credentials: 'include',
|
|
body: JSON.stringify({ newPassword: next }),
|
|
})
|
|
const data = await res.json()
|
|
if (!res.ok || data.error) {
|
|
setError(data.error ?? 'Passwort konnte nicht geändert werden.')
|
|
setLoading(false)
|
|
return
|
|
}
|
|
// Update local session state
|
|
if (sessionResult?.data?.user) {
|
|
const u = sessionResult.data.user as any
|
|
await setSession({ user: { id: u.id, email: u.email, name: u.name, mustChangePassword: false } })
|
|
}
|
|
} catch (e) {
|
|
setError('Verbindungsfehler. Bitte erneut versuchen.')
|
|
}
|
|
setLoading(false)
|
|
}
|
|
|
|
return (
|
|
<SafeAreaView style={fpc.safe}>
|
|
<ScrollView contentContainerStyle={fpc.content} keyboardShouldPersistTaps="handled">
|
|
<View style={fpc.card}>
|
|
<View style={fpc.iconWrap}>
|
|
<Ionicons name="lock-closed-outline" size={32} color="#003B7E" />
|
|
</View>
|
|
<Text style={fpc.title}>Passwort festlegen</Text>
|
|
<Text style={fpc.subtitle}>
|
|
Bitte legen Sie jetzt Ihr persönliches Passwort fest.
|
|
</Text>
|
|
|
|
<View style={fpc.field}>
|
|
<Text style={fpc.label}>Neues Passwort</Text>
|
|
<TextInput
|
|
style={fpc.input}
|
|
value={next}
|
|
onChangeText={setNext}
|
|
secureTextEntry
|
|
placeholder="Mindestens 8 Zeichen"
|
|
placeholderTextColor="#CBD5E1"
|
|
autoCapitalize="none"
|
|
/>
|
|
</View>
|
|
<View style={fpc.field}>
|
|
<Text style={fpc.label}>Neues Passwort wiederholen</Text>
|
|
<TextInput
|
|
style={fpc.input}
|
|
value={confirm}
|
|
onChangeText={setConfirm}
|
|
secureTextEntry
|
|
placeholder="Neues Passwort wiederholen"
|
|
placeholderTextColor="#CBD5E1"
|
|
autoCapitalize="none"
|
|
/>
|
|
</View>
|
|
|
|
{!!error && (
|
|
<View style={fpc.errorBox}>
|
|
<Text style={fpc.errorText}>{error}</Text>
|
|
</View>
|
|
)}
|
|
|
|
<TouchableOpacity style={fpc.btn} onPress={handleSubmit} disabled={loading}>
|
|
{loading
|
|
? <ActivityIndicator color="#fff" />
|
|
: <Text style={fpc.btnText}>Passwort festlegen</Text>
|
|
}
|
|
</TouchableOpacity>
|
|
|
|
<TouchableOpacity style={fpc.logoutBtn} onPress={() => void signOut()}>
|
|
<Text style={fpc.logoutText}>Abmelden</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</ScrollView>
|
|
</SafeAreaView>
|
|
)
|
|
}
|
|
|
|
const fpc = StyleSheet.create({
|
|
safe: { flex: 1, backgroundColor: '#F8FAFC' },
|
|
content: { flexGrow: 1, justifyContent: 'center', padding: 24, paddingBottom: 40 },
|
|
card: {
|
|
backgroundColor: '#FFFFFF', borderRadius: 20,
|
|
borderWidth: 1, borderColor: '#E2E8F0',
|
|
padding: 24, gap: 12,
|
|
},
|
|
iconWrap: {
|
|
width: 60, height: 60, borderRadius: 16,
|
|
backgroundColor: '#EFF6FF', alignItems: 'center', justifyContent: 'center',
|
|
alignSelf: 'center', marginBottom: 4,
|
|
},
|
|
title: { fontSize: 22, fontWeight: '800', color: '#0F172A', textAlign: 'center' },
|
|
subtitle: { fontSize: 13, color: '#64748B', textAlign: 'center', lineHeight: 19 },
|
|
field: { gap: 4 },
|
|
label: { fontSize: 12, fontWeight: '700', color: '#475569', textTransform: 'uppercase', letterSpacing: 0.5 },
|
|
input: {
|
|
borderWidth: 1, borderColor: '#E2E8F0', borderRadius: 10,
|
|
paddingHorizontal: 12, paddingVertical: 11,
|
|
fontSize: 14, color: '#0F172A', backgroundColor: '#F8FAFC',
|
|
},
|
|
errorBox: {
|
|
backgroundColor: '#FEF2F2', borderWidth: 1,
|
|
borderColor: '#FECACA', borderRadius: 10,
|
|
paddingHorizontal: 12, paddingVertical: 10,
|
|
},
|
|
errorText: { color: '#B91C1C', fontSize: 13 },
|
|
btn: {
|
|
backgroundColor: '#003B7E', borderRadius: 12,
|
|
paddingVertical: 13, alignItems: 'center', marginTop: 4,
|
|
},
|
|
btnText: { color: '#FFFFFF', fontWeight: '700', fontSize: 15 },
|
|
logoutBtn: { alignItems: 'center', paddingVertical: 8 },
|
|
logoutText: { color: '#94A3B8', fontSize: 13 },
|
|
})
|
|
|
|
export default function AppLayout() {
|
|
const session = useAuthStore((s) => s.session)
|
|
|
|
useEffect(() => {
|
|
if (!session?.user) return
|
|
setupPushNotifications().catch(() => {})
|
|
}, [session?.user?.id])
|
|
|
|
if (!session) {
|
|
return <Redirect href="/(auth)/login" />
|
|
}
|
|
|
|
if (session.user.mustChangePassword) {
|
|
return <ForcePasswordChangeScreen />
|
|
}
|
|
|
|
return (
|
|
<Tabs
|
|
screenOptions={{
|
|
tabBarActiveTintColor: '#003B7E',
|
|
tabBarInactiveTintColor: '#64748B',
|
|
tabBarStyle: {
|
|
borderTopWidth: 1,
|
|
borderTopColor: '#E2E8F0',
|
|
backgroundColor: '#FFFFFF',
|
|
height: Platform.OS === 'ios' ? 88 : 64,
|
|
paddingBottom: Platform.OS === 'ios' ? 28 : 8,
|
|
paddingTop: 8,
|
|
},
|
|
tabBarLabelStyle: {
|
|
fontSize: 11,
|
|
fontWeight: '600',
|
|
letterSpacing: 0.1,
|
|
},
|
|
headerShown: false,
|
|
}}
|
|
>
|
|
<Tabs.Screen
|
|
name="home/index"
|
|
options={{
|
|
title: 'Start',
|
|
tabBarIcon: ({ color, focused }) => (
|
|
<Ionicons name={focused ? 'home' : 'home-outline'} size={23} color={color} />
|
|
),
|
|
}}
|
|
/>
|
|
<Tabs.Screen
|
|
name="news/index"
|
|
options={{
|
|
title: 'Aktuelles',
|
|
tabBarIcon: ({ color, focused }) => (
|
|
<Ionicons name={focused ? 'newspaper' : 'newspaper-outline'} size={23} color={color} />
|
|
),
|
|
}}
|
|
/>
|
|
<Tabs.Screen
|
|
name="termine/index"
|
|
options={{
|
|
title: 'Termine',
|
|
tabBarIcon: ({ color, focused }) => (
|
|
<Ionicons name={focused ? 'calendar' : 'calendar-outline'} size={23} color={color} />
|
|
),
|
|
}}
|
|
/>
|
|
<Tabs.Screen
|
|
name="stellen/index"
|
|
options={{
|
|
title: 'Stellen',
|
|
tabBarIcon: ({ color, focused }) => (
|
|
<Ionicons name={focused ? 'briefcase' : 'briefcase-outline'} size={23} color={color} />
|
|
),
|
|
}}
|
|
/>
|
|
<Tabs.Screen
|
|
name="chat/index"
|
|
options={{
|
|
title: 'Nachrichten',
|
|
tabBarIcon: ({ color, focused }) => (
|
|
<ChatTabIcon color={color} focused={focused} />
|
|
),
|
|
}}
|
|
/>
|
|
<Tabs.Screen
|
|
name="profil/index"
|
|
options={{
|
|
title: 'Profil',
|
|
tabBarIcon: ({ color, focused }) => (
|
|
<Ionicons name={focused ? 'person' : 'person-outline'} size={23} color={color} />
|
|
),
|
|
}}
|
|
/>
|
|
|
|
<Tabs.Screen name="members/index" options={{ href: null }} />
|
|
<Tabs.Screen name="news/[id]" options={{ href: null }} />
|
|
<Tabs.Screen name="members/[id]" options={{ href: null }} />
|
|
<Tabs.Screen name="termine/[id]" options={{ href: null }} />
|
|
<Tabs.Screen name="stellen/[id]" options={{ href: null }} />
|
|
<Tabs.Screen name="chat/[id]" options={{ href: null }} />
|
|
</Tabs>
|
|
)
|
|
}
|