Mobile Dev
All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 2m50s
Build and Push Docker Images / build-frontend (push) Successful in 1m13s

This commit is contained in:
2026-02-21 21:21:39 +01:00
parent 30d761f899
commit 409db82368
16 changed files with 2663 additions and 689 deletions

View File

@@ -1,15 +1,31 @@
import { useState } from 'react';
import { useMemo, useState } from 'react';
import { Redirect, router } from 'expo-router';
import { ActivityIndicator, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
import { ActivityIndicator, Pressable, ScrollView, StyleSheet, Text, TextInput, View } from 'react-native';
import { useAuth } from '@/src/providers/auth-provider';
import { useAuth, useTokenStorageInfo } from '@/src/providers/auth-provider';
const PRIMARY = '#69c350';
export default function TenantSelectScreen() {
const { token, tenants, activeTenantId, requiresTenantSelection, switchTenant } = useAuth();
const { token, tenants, activeTenantId, requiresTenantSelection, switchTenant, user, logout } = useAuth();
const storageInfo = useTokenStorageInfo();
const [switchingTenantId, setSwitchingTenantId] = useState<number | null>(null);
const [search, setSearch] = useState('');
const [error, setError] = useState<string | null>(null);
const filteredTenants = useMemo(() => {
const terms = search.trim().toLowerCase().split(/\s+/).filter(Boolean);
if (terms.length === 0) return tenants;
return tenants.filter((tenant) => {
const haystack = `${String(tenant.name || '').toLowerCase()} ${String(tenant.id || '').toLowerCase()} ${
String(tenant.short || '').toLowerCase()
}`;
return terms.every((term) => haystack.includes(term));
});
}, [search, tenants]);
if (!token) {
return <Redirect href="/login" />;
}
@@ -32,14 +48,34 @@ export default function TenantSelectScreen() {
}
}
async function onLogout() {
setSwitchingTenantId(null);
await logout();
router.replace('/login');
}
return (
<ScrollView contentContainerStyle={styles.container}>
<Text style={styles.title}>Tenant auswaehlen</Text>
<Text style={styles.subtitle}>Bitte waehle den Tenant fuer deine Session.</Text>
<View style={styles.headerCard}>
<Text style={styles.title}>Tenant auswählen</Text>
<Text style={styles.subtitle}>Wähle den Mandanten für diese Session.</Text>
<Text style={styles.meta}>User: {String(user?.email || user?.id || 'unbekannt')}</Text>
<Text style={styles.meta}>Storage: {storageInfo.mode}</Text>
</View>
{error ? <Text style={styles.error}>{error}</Text> : null}
{tenants.map((tenant) => {
<TextInput
value={search}
onChangeText={setSearch}
placeholder="Tenant suchen"
placeholderTextColor="#9ca3af"
style={styles.searchInput}
/>
{filteredTenants.length === 0 ? <Text style={styles.empty}>Keine passenden Tenants gefunden.</Text> : null}
{filteredTenants.map((tenant) => {
const tenantId = Number(tenant.id);
const isBusy = switchingTenantId === tenantId;
@@ -49,14 +85,20 @@ export default function TenantSelectScreen() {
style={[styles.tenantButton, isBusy ? styles.tenantButtonDisabled : null]}
onPress={() => onSelectTenant(tenantId)}
disabled={switchingTenantId !== null}>
<View>
<Text style={styles.tenantName}>{tenant.name}</Text>
<View style={styles.tenantInfo}>
<Text style={styles.tenantName} numberOfLines={1}>
{tenant.name}
</Text>
<Text style={styles.tenantMeta}>ID: {tenantId}</Text>
</View>
{isBusy ? <ActivityIndicator /> : <Text style={styles.tenantAction}>Auswaehlen</Text>}
{isBusy ? <ActivityIndicator color={PRIMARY} /> : <Text style={styles.tenantAction}>Auswählen</Text>}
</Pressable>
);
})}
<Pressable style={styles.logoutButton} onPress={onLogout} disabled={switchingTenantId !== null}>
<Text style={styles.logoutText}>Anderen Nutzer anmelden</Text>
</Pressable>
</ScrollView>
);
}
@@ -65,21 +107,43 @@ const styles = StyleSheet.create({
container: {
flexGrow: 1,
padding: 16,
gap: 10,
gap: 12,
backgroundColor: '#f9fafb',
},
headerCard: {
borderRadius: 12,
borderWidth: 1,
borderColor: '#e5e7eb',
backgroundColor: '#ffffff',
padding: 14,
gap: 4,
},
title: {
fontSize: 24,
fontSize: 22,
fontWeight: '700',
color: '#111827',
},
subtitle: {
color: '#6b7280',
marginBottom: 8,
marginBottom: 2,
},
meta: {
color: '#6b7280',
fontSize: 12,
},
error: {
color: '#dc2626',
marginBottom: 8,
fontSize: 13,
},
searchInput: {
borderWidth: 1,
borderColor: '#d1d5db',
borderRadius: 10,
paddingHorizontal: 12,
paddingVertical: 10,
fontSize: 15,
color: '#111827',
backgroundColor: '#ffffff',
},
tenantButton: {
borderRadius: 12,
@@ -94,6 +158,11 @@ const styles = StyleSheet.create({
tenantButtonDisabled: {
opacity: 0.6,
},
tenantInfo: {
flex: 1,
minWidth: 0,
paddingRight: 10,
},
tenantName: {
fontSize: 16,
fontWeight: '600',
@@ -104,7 +173,27 @@ const styles = StyleSheet.create({
marginTop: 4,
},
tenantAction: {
color: '#2563eb',
color: PRIMARY,
fontWeight: '600',
},
empty: {
color: '#6b7280',
fontSize: 13,
textAlign: 'center',
paddingVertical: 8,
},
logoutButton: {
marginTop: 4,
minHeight: 42,
borderRadius: 10,
borderWidth: 1,
borderColor: '#d1d5db',
backgroundColor: '#ffffff',
alignItems: 'center',
justifyContent: 'center',
},
logoutText: {
color: '#374151',
fontWeight: '600',
},
});