refactor: restructure

monorepo with apps/ and services/
  directories
This commit is contained in:
Wuesteon 2025-11-26 03:03:24 +01:00
parent 25824ed0ac
commit ff80aeec1f
4062 changed files with 2592 additions and 1278 deletions

View file

@ -0,0 +1,245 @@
import React, { useState, useEffect } from 'react';
import { View, Text, TextInput, TouchableOpacity, StyleSheet, Alert } from 'react-native';
import { supabase } from '../utils/supabase';
import { Session } from '@supabase/supabase-js';
interface Profile {
id: string;
first_name: string | null;
last_name: string | null;
avatar_url: string | null;
is_individual: boolean;
individual_quota: number;
individual_usage: number;
}
export default function Account({ session }: { session: Session }) {
const [loading, setLoading] = useState(true);
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [profile, setProfile] = useState<Profile | null>(null);
useEffect(() => {
if (session) getProfile();
}, [session]);
async function getProfile() {
try {
setLoading(true);
const { user } = session;
const { data, error } = await supabase
.from('profiles')
.select('*')
.eq('id', user.id)
.single();
if (error) {
throw error;
}
if (data) {
setProfile(data);
setFirstName(data.first_name || '');
setLastName(data.last_name || '');
}
} catch (error) {
if (error instanceof Error) {
Alert.alert('Fehler beim Laden des Profils', error.message);
}
} finally {
setLoading(false);
}
}
async function updateProfile() {
try {
setLoading(true);
const { user } = session;
// Prüfen, ob das Profil bereits existiert
if (!profile) {
// Erstelle ein neues Profil, wenn es noch nicht existiert
const { error: insertError } = await supabase
.from('profiles')
.insert([
{
id: user.id,
first_name: firstName,
last_name: lastName,
is_individual: true, // Standardmäßig als Einzelnutzer
individual_quota: 0,
individual_usage: 0,
},
]);
if (insertError) throw insertError;
} else {
// Aktualisiere das bestehende Profil
const { error: updateError } = await supabase
.from('profiles')
.update({
first_name: firstName,
last_name: lastName,
updated_at: new Date(),
})
.eq('id', user.id);
if (updateError) throw updateError;
}
Alert.alert('Erfolg', 'Profil erfolgreich aktualisiert!');
} catch (error) {
if (error instanceof Error) {
Alert.alert('Fehler beim Aktualisieren des Profils', error.message);
}
} finally {
setLoading(false);
}
}
async function signOut() {
try {
setLoading(true);
const { error } = await supabase.auth.signOut();
if (error) throw error;
} catch (error) {
if (error instanceof Error) {
Alert.alert('Fehler beim Abmelden', error.message);
}
} finally {
setLoading(false);
}
}
return (
<View style={styles.container}>
<View style={styles.profileContainer}>
<Text style={styles.header}>Mein Profil</Text>
<View style={styles.formGroup}>
<Text style={styles.label}>E-Mail</Text>
<Text style={styles.value}>{session?.user?.email}</Text>
</View>
<View style={styles.formGroup}>
<Text style={styles.label}>Vorname</Text>
<TextInput
style={styles.input}
value={firstName}
onChangeText={setFirstName}
placeholder="Vorname eingeben"
/>
</View>
<View style={styles.formGroup}>
<Text style={styles.label}>Nachname</Text>
<TextInput
style={styles.input}
value={lastName}
onChangeText={setLastName}
placeholder="Nachname eingeben"
/>
</View>
{profile && profile.is_individual && (
<View style={styles.quotaContainer}>
<Text style={styles.label}>Verfügbare Kredite</Text>
<Text style={styles.quota}>
{profile.individual_quota - profile.individual_usage} / {profile.individual_quota}
</Text>
</View>
)}
<TouchableOpacity
style={styles.button}
onPress={updateProfile}
disabled={loading}
>
<Text style={styles.buttonText}>
{loading ? 'Wird aktualisiert...' : 'Profil aktualisieren'}
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.button, styles.signOutButton]}
onPress={signOut}
disabled={loading}
>
<Text style={styles.buttonText}>Abmelden</Text>
</TouchableOpacity>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
},
profileContainer: {
backgroundColor: 'white',
padding: 20,
borderRadius: 10,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
header: {
fontSize: 24,
fontWeight: 'bold',
marginBottom: 20,
},
formGroup: {
marginBottom: 15,
},
label: {
fontSize: 16,
marginBottom: 5,
fontWeight: '500',
},
value: {
fontSize: 16,
color: '#666',
paddingVertical: 10,
},
input: {
height: 50,
borderWidth: 1,
borderColor: '#ddd',
borderRadius: 5,
paddingHorizontal: 10,
backgroundColor: '#f9f9f9',
},
quotaContainer: {
marginVertical: 15,
padding: 15,
backgroundColor: '#f0f8ff',
borderRadius: 5,
},
quota: {
fontSize: 18,
fontWeight: 'bold',
color: '#0055FF',
},
button: {
backgroundColor: '#0055FF',
height: 50,
borderRadius: 5,
justifyContent: 'center',
alignItems: 'center',
marginTop: 15,
},
buttonText: {
color: 'white',
fontSize: 16,
fontWeight: 'bold',
},
signOutButton: {
backgroundColor: '#ff3b30',
marginTop: 10,
},
});

View file

@ -0,0 +1,329 @@
import React, { useState } from 'react';
import { Alert, StyleSheet, View, TextInput, TouchableOpacity, Text, Image, Platform } from 'react-native';
import { FontAwesome5 } from '@expo/vector-icons';
import { supabase } from '../utils/supabase';
import { useTheme, useThemeColors, lightColors, darkColors } from '../utils/themeContext';
export default function Auth() {
const { isDarkMode } = useTheme();
const themeColors = useThemeColors();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const [isSignUp, setIsSignUp] = useState(false);
const [isResetPassword, setIsResetPassword] = useState(false);
async function signInWithEmail() {
setLoading(true);
const { error } = await supabase.auth.signInWithPassword({
email,
password,
});
if (error) {
Alert.alert('Fehler bei der Anmeldung', error.message);
}
setLoading(false);
}
async function signUpWithEmail() {
setLoading(true);
const { error } = await supabase.auth.signUp({
email,
password,
});
if (error) {
Alert.alert('Fehler bei der Registrierung', error.message);
} else {
Alert.alert(
'Registrierung erfolgreich',
'Bitte überprüfen Sie Ihre E-Mail für den Bestätigungslink.'
);
}
setLoading(false);
}
async function resetPassword() {
console.log('Reset password called with email:', email);
if (!email) {
Alert.alert('Fehler', 'Bitte geben Sie Ihre E-Mail-Adresse ein');
return;
}
setLoading(true);
try {
const apiUrl = process.env.EXPO_PUBLIC_API_URL || 'https://mana-core-middleware-111768794939.europe-west3.run.app';
const endpoint = `${apiUrl}/auth/reset-password`;
console.log('Calling API endpoint:', endpoint);
console.log('Request body:', { email });
// Call your backend endpoint for password reset
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email }),
});
console.log('Response status:', response.status);
const data = await response.json();
console.log('Response data:', data);
if (!response.ok) {
throw new Error(data.error || 'Fehler beim Zurücksetzen des Passworts');
}
Alert.alert(
'E-Mail gesendet',
'Bitte überprüfen Sie Ihre E-Mail für den Link zum Zurücksetzen des Passworts.'
);
setIsResetPassword(false);
} catch (error: any) {
console.error('Reset password error:', error);
Alert.alert('Fehler', error.message || 'Netzwerkfehler. Bitte versuchen Sie es später erneut.');
}
setLoading(false);
}
return (
<View style={[styles.container, { backgroundColor: isDarkMode ? '#121212' : '#F5F5F5' }]}>
<View style={[styles.formContainer, { backgroundColor: isDarkMode ? '#1E1E1E' : '#FFFFFF', borderColor: isDarkMode ? '#374151' : '#E5E7EB' }]}>
<View style={styles.logoContainer}>
<FontAwesome5 name="fire" size={40} color="#F59E0B" style={styles.logoIcon} />
<Text style={[styles.logoText, { color: isDarkMode ? '#F9FAFB' : '#1F2937' }]}>ManaCore</Text>
</View>
<Text style={[styles.header, { color: isDarkMode ? '#F9FAFB' : '#1F2937' }]}>
{isResetPassword ? 'Passwort zurücksetzen' : isSignUp ? 'Registrieren' : 'Anmelden'}
</Text>
<Text style={[styles.subHeader, { color: isDarkMode ? '#9CA3AF' : '#6B7280' }]}>
{isResetPassword
? 'Geben Sie Ihre E-Mail-Adresse ein, um Ihr Passwort zurückzusetzen'
: isSignUp
? 'Erstellen Sie ein neues Konto, um Mana zu teilen und zu verwalten'
: 'Melden Sie sich an, um Ihre Mana zu verwalten'}
</Text>
<View style={styles.inputContainer}>
<FontAwesome5 name="envelope" size={16} color={isDarkMode ? '#9CA3AF' : '#6B7280'} style={styles.inputIcon} />
<TextInput
style={[styles.input, {
backgroundColor: isDarkMode ? '#374151' : '#F9FAFB',
borderColor: isDarkMode ? '#4B5563' : '#E5E7EB',
color: isDarkMode ? '#F9FAFB' : '#1F2937'
}]}
placeholder="E-Mail"
placeholderTextColor={isDarkMode ? '#9CA3AF' : '#6B7280'}
value={email}
onChangeText={setEmail}
autoCapitalize="none"
keyboardType="email-address"
/>
</View>
{!isResetPassword && (
<View style={styles.inputContainer}>
<FontAwesome5 name="lock" size={16} color={isDarkMode ? '#9CA3AF' : '#6B7280'} style={styles.inputIcon} />
<TextInput
style={[styles.input, {
backgroundColor: isDarkMode ? '#374151' : '#F9FAFB',
borderColor: isDarkMode ? '#4B5563' : '#E5E7EB',
color: isDarkMode ? '#F9FAFB' : '#1F2937'
}]}
placeholder="Passwort"
placeholderTextColor={isDarkMode ? '#9CA3AF' : '#6B7280'}
value={password}
onChangeText={setPassword}
secureTextEntry
autoCapitalize="none"
/>
</View>
)}
<TouchableOpacity
style={[styles.button, {
backgroundColor: themeColors.primary,
opacity: loading ? 0.7 : 1
}]}
onPress={() => {
if (isResetPassword) {
resetPassword();
} else if (isSignUp) {
signUpWithEmail();
} else {
signInWithEmail();
}
}}
disabled={loading}
>
{loading ? (
<View style={styles.loadingContainer}>
<Text style={styles.buttonText}>Lädt</Text>
<View style={styles.dotsContainer}>
<Text style={styles.dots}>.</Text>
<Text style={styles.dots}>.</Text>
<Text style={styles.dots}>.</Text>
</View>
</View>
) : (
<Text style={styles.buttonText}>
{isResetPassword ? 'Link senden' : isSignUp ? 'Registrieren' : 'Anmelden'}
</Text>
)}
</TouchableOpacity>
{Platform.OS === 'web' && !isSignUp && !isResetPassword && (
<TouchableOpacity
style={styles.forgotPasswordButton}
onPress={() => setIsResetPassword(true)}
>
<Text style={[styles.forgotPasswordText, { color: themeColors.primary }]}>
Passwort vergessen?
</Text>
</TouchableOpacity>
)}
<TouchableOpacity
style={styles.switchButton}
onPress={() => {
if (isResetPassword) {
setIsResetPassword(false);
} else {
setIsSignUp(!isSignUp);
}
}}
>
<Text style={[styles.switchText, { color: themeColors.primary }]}>
{isResetPassword
? 'Zurück zur Anmeldung'
: isSignUp
? 'Bereits ein Konto? Anmelden'
: 'Kein Konto? Registrieren'}
</Text>
</TouchableOpacity>
<View style={styles.footer}>
<Text style={[styles.footerText, { color: isDarkMode ? '#9CA3AF' : '#6B7280' }]}>
© {new Date().getFullYear()} ManaCore. Alle Rechte vorbehalten.
</Text>
</View>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
justifyContent: 'center',
},
formContainer: {
padding: 24,
borderRadius: 12,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
borderWidth: 1,
maxWidth: 500,
width: '100%',
alignSelf: 'center',
},
logoContainer: {
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
marginBottom: 24,
},
logoIcon: {
marginRight: 12,
},
logoText: {
fontSize: 28,
fontWeight: 'bold',
},
header: {
fontSize: 24,
fontWeight: 'bold',
marginBottom: 8,
textAlign: 'center',
},
subHeader: {
fontSize: 16,
textAlign: 'center',
marginBottom: 24,
},
inputContainer: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 16,
},
inputIcon: {
marginRight: 12,
},
input: {
flex: 1,
height: 50,
borderWidth: 1,
borderRadius: 8,
paddingHorizontal: 16,
fontSize: 16,
},
button: {
height: 54,
borderRadius: 8,
justifyContent: 'center',
alignItems: 'center',
marginTop: 24,
},
buttonText: {
color: 'white',
fontSize: 18,
fontWeight: 'bold',
},
loadingContainer: {
flexDirection: 'row',
alignItems: 'center',
},
dotsContainer: {
flexDirection: 'row',
marginLeft: 4,
},
dots: {
color: 'white',
fontSize: 18,
fontWeight: 'bold',
marginLeft: 2,
},
switchButton: {
marginTop: 24,
alignItems: 'center',
},
switchText: {
fontSize: 16,
},
forgotPasswordButton: {
marginTop: 16,
alignItems: 'center',
},
forgotPasswordText: {
fontSize: 14,
},
footer: {
marginTop: 40,
alignItems: 'center',
},
footerText: {
fontSize: 12,
},
});

View file

@ -0,0 +1,22 @@
import { forwardRef } from 'react';
import { Text, TouchableOpacity, TouchableOpacityProps, View } from 'react-native';
type ButtonProps = {
title: string;
} & TouchableOpacityProps;
export const Button = forwardRef<View, ButtonProps>(({ title, ...touchableProps }, ref) => {
return (
<TouchableOpacity
ref={ref}
{...touchableProps}
className={`${styles.button} ${touchableProps.className}`}>
<Text className={styles.buttonText}>{title}</Text>
</TouchableOpacity>
);
});
const styles = {
button: 'items-center bg-indigo-500 rounded-[28px] shadow-md p-4',
buttonText: 'text-white text-lg font-semibold text-center',
};

View file

@ -0,0 +1,12 @@
import { SafeAreaView, View } from 'react-native';
import { useTheme } from '~/utils/themeContext';
export const Container = ({ children }: { children: React.ReactNode }) => {
const { isDarkMode } = useTheme();
return (
<SafeAreaView className={`flex flex-1 ${isDarkMode ? 'bg-gray-900' : 'bg-white'}`}>
<View className="flex flex-1 m-6">{children}</View>
</SafeAreaView>
);
};

View file

@ -0,0 +1,243 @@
import React, { useState, useEffect } from 'react';
import { View, Text, TextInput, TouchableOpacity, Alert, ActivityIndicator, ScrollView } from 'react-native';
import { supabase } from '../utils/supabase';
import { Session } from '@supabase/supabase-js';
import { useTheme } from '../utils/themeContext';
interface UserRole {
role_id: string;
roles?: {
name: string;
};
}
interface CreateOrganizationProps {
onOrgCreated?: (orgId: string, orgName: string) => void;
}
export default function CreateOrganization({ onOrgCreated }: CreateOrganizationProps) {
const [session, setSession] = useState<Session | null>(null);
const [loading, setLoading] = useState(false);
const [organizationName, setOrganizationName] = useState('');
const [initialCredits, setInitialCredits] = useState('');
const [userHasPermission, setUserHasPermission] = useState(false);
const [checkingPermission, setCheckingPermission] = useState(true);
const { isDarkMode } = useTheme();
useEffect(() => {
// Prüfe den aktuellen Authentifizierungsstatus
supabase.auth.getSession().then(({ data: { session } }) => {
setSession(session);
if (session) {
checkUserPermission(session.user.id);
} else {
setCheckingPermission(false);
}
});
// Abonniere Authentifizierungsänderungen
const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => {
setSession(session);
if (session) {
checkUserPermission(session.user.id);
} else {
setCheckingPermission(false);
}
});
return () => subscription.unsubscribe();
}, []);
async function checkUserPermission(userId: string) {
try {
setCheckingPermission(true);
// Prüfe, ob der Benutzer ein System-Administrator ist
const { data: userRoles, error: userRolesError } = await supabase
.from('user_roles')
.select('role_id, roles(name)')
.eq('user_id', userId) as { data: UserRole[] | null, error: any };
if (userRolesError) throw userRolesError;
if (userRoles && userRoles.length > 0) {
// Prüfe, ob der Benutzer die Rolle "system_admin" hat
const isSystemAdmin = userRoles.some(role => {
const roleName = role.roles?.name;
return roleName === 'system_admin';
});
setUserHasPermission(isSystemAdmin);
} else {
setUserHasPermission(false);
}
} catch (error) {
console.error('Fehler beim Prüfen der Benutzerberechtigungen:', error);
setUserHasPermission(false);
} finally {
setCheckingPermission(false);
}
}
async function createOrganization() {
if (!session) {
Alert.alert('Fehler', 'Sie müssen angemeldet sein, um eine Organisation zu erstellen.');
return;
}
if (!userHasPermission) {
Alert.alert('Fehler', 'Sie haben keine Berechtigung, Organisationen zu erstellen.');
return;
}
if (!organizationName.trim()) {
Alert.alert('Fehler', 'Bitte geben Sie einen Organisationsnamen ein.');
return;
}
const credits = parseInt(initialCredits);
if (isNaN(credits) || credits < 0) {
Alert.alert('Fehler', 'Bitte geben Sie eine gültige Anzahl an Krediten ein.');
return;
}
try {
setLoading(true);
// 1. Erstelle die Organisation
const { data: organization, error: orgError } = await supabase
.from('organizations')
.insert([
{
name: organizationName.trim(),
total_credits: credits,
used_credits: 0
}
])
.select()
.single();
if (orgError) throw orgError;
// 2. Hole die org_admin Rolle
const { data: adminRole, error: roleError } = await supabase
.from('roles')
.select('id')
.eq('name', 'org_admin')
.single();
if (roleError) throw roleError;
// 3. Füge den aktuellen Benutzer als Organisations-Administrator hinzu
const { error: userRoleError } = await supabase
.from('user_roles')
.insert([
{
user_id: session.user.id,
role_id: adminRole.id,
organization_id: organization.id
}
]);
if (userRoleError) throw userRoleError;
// Erfolgsbenachrichtigung anzeigen und direkt navigieren
Alert.alert(
'Erfolg',
`Die Organisation "${organizationName}" wurde erfolgreich erstellt.`
);
// Direkt zur Organisationsdetailseite navigieren, ohne auf OK zu warten
if (onOrgCreated) {
console.log('Navigiere zur neuen Organisationsseite:', organization.id, organization.name);
onOrgCreated(organization.id, organization.name);
}
// Formular zurücksetzen
setOrganizationName('');
setInitialCredits('');
} catch (error) {
console.error('Fehler beim Erstellen der Organisation:', error);
Alert.alert('Fehler', 'Es ist ein Fehler beim Erstellen der Organisation aufgetreten.');
} finally {
setLoading(false);
}
}
if (!session) {
return (
<View className={`${isDarkMode ? 'bg-gray-800' : 'bg-white'} rounded-lg p-5 m-2.5 shadow`}>
<Text className={`text-base ${isDarkMode ? 'text-gray-300' : 'text-gray-600'} text-center p-5`}>
Bitte melden Sie sich an, um Organisationen zu erstellen.
</Text>
</View>
);
}
if (checkingPermission) {
return (
<View className={`${isDarkMode ? 'bg-gray-800' : 'bg-white'} rounded-lg p-5 m-2.5 shadow`}>
<ActivityIndicator size="large" color={isDarkMode ? '#93C5FD' : '#0055FF'} />
<Text className={`text-base ${isDarkMode ? 'text-gray-300' : 'text-gray-600'} text-center mt-2.5`}>Prüfe Berechtigungen...</Text>
</View>
);
}
if (!userHasPermission) {
return (
<View className={`${isDarkMode ? 'bg-gray-800' : 'bg-white'} rounded-lg p-5 m-2.5 shadow`}>
<Text className={`text-base ${isDarkMode ? 'text-gray-300' : 'text-gray-600'} text-center p-5`}>
Sie haben keine Berechtigung, Organisationen zu erstellen. Bitte kontaktieren Sie einen Administrator.
</Text>
</View>
);
}
return (
<ScrollView className="flex-1">
<View className={`${isDarkMode ? 'bg-gray-800' : 'bg-white'} rounded-lg p-5 m-2.5 shadow`}>
<Text className={`text-2xl font-bold mb-5 ${isDarkMode ? 'text-gray-100' : 'text-gray-800'}`}>Neue Organisation erstellen</Text>
<View className="mb-4">
<Text className={`text-base font-medium mb-2 ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}>Organisationsname</Text>
<TextInput
className={`h-12 border ${isDarkMode ? 'border-gray-600 bg-gray-700 text-white' : 'border-gray-300 bg-gray-50 text-gray-800'} rounded-lg px-4 text-base`}
value={organizationName}
onChangeText={setOrganizationName}
placeholder="Name der Organisation eingeben"
placeholderTextColor={isDarkMode ? '#9CA3AF' : '#9CA3AF'}
/>
</View>
<View className="mb-4">
<Text className={`text-base font-medium mb-2 ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}>Anfängliche Kredite</Text>
<TextInput
className={`h-12 border ${isDarkMode ? 'border-gray-600 bg-gray-700 text-white' : 'border-gray-300 bg-gray-50 text-gray-800'} rounded-lg px-4 text-base`}
value={initialCredits}
onChangeText={setInitialCredits}
placeholder="Anzahl der Kredite eingeben"
placeholderTextColor={isDarkMode ? '#9CA3AF' : '#9CA3AF'}
keyboardType="number-pad"
/>
<Text className={`text-sm mt-1 ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`}>
Dies ist die Gesamtanzahl der Kredite, die dieser Organisation zur Verfügung stehen werden.
</Text>
</View>
<TouchableOpacity
className={`${isDarkMode ? 'bg-blue-700' : 'bg-blue-600'} py-3 px-4 rounded-lg ${loading ? 'opacity-70' : ''}`}
onPress={createOrganization}
disabled={loading}
>
{loading ? (
<ActivityIndicator size="small" color="white" />
) : (
<Text className="text-white font-semibold text-center text-base">Organisation erstellen</Text>
)}
</TouchableOpacity>
</View>
</ScrollView>
);
}
// NativeWind wird für das Styling verwendet, daher sind keine StyleSheet-Definitionen erforderlich

View file

@ -0,0 +1,326 @@
import React, { useState, useEffect } from 'react';
import { View, Text, TextInput, TouchableOpacity, Alert, ActivityIndicator, ScrollView } from 'react-native';
import { supabase } from '../utils/supabase';
import { Session } from '@supabase/supabase-js';
import { useTheme } from '../utils/themeContext';
import { useRouter } from 'expo-router';
interface Organization {
id: string;
name: string;
}
interface UserRole {
organization_id: string;
roles?: {
name: string;
};
}
interface CreateTeamProps {
onTeamCreated?: (teamId: string, teamName: string) => void;
}
export default function CreateTeam({ onTeamCreated }: CreateTeamProps) {
const router = useRouter();
const [session, setSession] = useState<Session | null>(null);
const [loading, setLoading] = useState(false);
const [organizations, setOrganizations] = useState<Organization[]>([]);
const [selectedOrgId, setSelectedOrgId] = useState<string | null>(null);
const [teamName, setTeamName] = useState('');
const [allocatedCredits, setAllocatedCredits] = useState('');
const [fetchingOrgs, setFetchingOrgs] = useState(true);
const { isDarkMode } = useTheme();
useEffect(() => {
// Prüfe den aktuellen Authentifizierungsstatus
supabase.auth.getSession().then(({ data: { session } }) => {
setSession(session);
if (session) {
fetchUserOrganizations(session.user.id);
} else {
setFetchingOrgs(false);
}
});
// Abonniere Authentifizierungsänderungen
const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => {
setSession(session);
if (session) {
fetchUserOrganizations(session.user.id);
} else {
setFetchingOrgs(false);
}
});
return () => subscription.unsubscribe();
}, []);
async function fetchUserOrganizations(userId: string) {
try {
setFetchingOrgs(true);
// Suche nach Organisationen, in denen der Benutzer eine Rolle hat
const { data: userRoles, error: userRolesError } = await supabase
.from('user_roles')
.select('organization_id, roles(name)')
.eq('user_id', userId)
.not('organization_id', 'is', null) as { data: UserRole[] | null, error: any };
if (userRolesError) throw userRolesError;
if (userRoles && userRoles.length > 0) {
// Extrahiere die Organisations-IDs
const orgIds = userRoles
.filter(role => {
// Prüfe, ob der Benutzer ein Administrator oder Manager ist
const roleName = role.roles?.name;
return roleName === 'system_admin' ||
roleName === 'org_admin' ||
roleName === 'team_admin';
})
.map(role => role.organization_id);
if (orgIds.length > 0) {
// Hole die Details der Organisationen
const { data: orgs, error: orgsError } = await supabase
.from('organizations')
.select('id, name')
.in('id', orgIds);
if (orgsError) throw orgsError;
if (orgs && orgs.length > 0) {
setOrganizations(orgs);
// Setze die erste Organisation als Standard
setSelectedOrgId(orgs[0].id);
}
}
}
} catch (error) {
console.error('Fehler beim Abrufen der Organisationen:', error);
Alert.alert('Fehler', 'Es ist ein Fehler beim Laden der Organisationen aufgetreten.');
} finally {
setFetchingOrgs(false);
}
}
async function createTeam() {
if (!session) {
Alert.alert('Fehler', 'Sie müssen angemeldet sein, um ein Team zu erstellen.');
return;
}
if (!selectedOrgId) {
Alert.alert('Fehler', 'Bitte wählen Sie eine Organisation aus.');
return;
}
if (!teamName.trim()) {
Alert.alert('Fehler', 'Bitte geben Sie einen Teamnamen ein.');
return;
}
const credits = parseInt(allocatedCredits);
if (isNaN(credits) || credits < 0) {
Alert.alert('Fehler', 'Bitte geben Sie eine gültige Anzahl an Krediten ein.');
return;
}
try {
setLoading(true);
// 1. Prüfe, ob die Organisation genügend Kredite hat
const { data: org, error: orgError } = await supabase
.from('organizations')
.select('total_credits, used_credits')
.eq('id', selectedOrgId)
.single();
if (orgError) throw orgError;
const availableCredits = org.total_credits - org.used_credits;
if (credits > availableCredits) {
Alert.alert('Fehler', `Die Organisation hat nicht genügend Kredite. Verfügbar: ${availableCredits}`);
return;
}
// 2. Erstelle das Team
const { data: team, error: teamError } = await supabase
.from('teams')
.insert([
{
organization_id: selectedOrgId,
name: teamName.trim(),
allocated_credits: credits
}
])
.select()
.single();
if (teamError) throw teamError;
// 3. Aktualisiere die verwendeten Kredite der Organisation
const { error: updateOrgError } = await supabase
.from('organizations')
.update({
used_credits: org.used_credits + credits
})
.eq('id', selectedOrgId);
if (updateOrgError) throw updateOrgError;
// 4. Füge den aktuellen Benutzer als Team-Administrator hinzu
// Zuerst hole die team_admin Rolle
const { data: adminRole, error: roleError } = await supabase
.from('roles')
.select('id')
.eq('name', 'team_admin')
.single();
if (roleError) throw roleError;
// Füge die Benutzerrolle hinzu
const { error: userRoleError } = await supabase
.from('user_roles')
.insert([
{
user_id: session.user.id,
role_id: adminRole.id,
team_id: team.id,
organization_id: selectedOrgId
}
]);
if (userRoleError) throw userRoleError;
// 5. Füge den Benutzer als Teammitglied hinzu
const { error: teamMemberError } = await supabase
.from('team_members')
.insert([
{
team_id: team.id,
user_id: session.user.id,
allocated_credits: 0, // Standardmäßig keine Kredite zugewiesen
used_credits: 0
}
]);
if (teamMemberError) throw teamMemberError;
// Erfolgsbenachrichtigung anzeigen und direkt navigieren
Alert.alert(
'Erfolg',
`Das Team "${teamName}" wurde erfolgreich erstellt.`
);
// Direkt zur Teamdetailseite navigieren, ohne auf OK zu warten
if (onTeamCreated) {
console.log('Navigiere zur neuen Teamseite:', team.id, team.name);
onTeamCreated(team.id, team.name);
}
// Formular zurücksetzen
setTeamName('');
setAllocatedCredits('');
} catch (error) {
console.error('Fehler beim Erstellen des Teams:', error);
Alert.alert('Fehler', 'Es ist ein Fehler beim Erstellen des Teams aufgetreten.');
} finally {
setLoading(false);
}
}
if (!session) {
return (
<View className={`${isDarkMode ? 'bg-gray-800' : 'bg-white'} rounded-lg p-5 m-2.5 shadow`}>
<Text className={`text-base ${isDarkMode ? 'text-gray-300' : 'text-gray-600'} text-center p-5`}>
Bitte melden Sie sich an, um Teams zu erstellen.
</Text>
</View>
);
}
if (fetchingOrgs) {
return (
<View className={`${isDarkMode ? 'bg-gray-800' : 'bg-white'} rounded-lg p-5 m-2.5 shadow`}>
<ActivityIndicator size="large" color={isDarkMode ? '#93C5FD' : '#0055FF'} />
<Text className={`text-base ${isDarkMode ? 'text-gray-300' : 'text-gray-600'} text-center mt-2.5`}>Lade Organisationen...</Text>
</View>
);
}
if (organizations.length === 0) {
return (
<View className={`${isDarkMode ? 'bg-gray-800' : 'bg-white'} rounded-lg p-5 m-2.5 shadow`}>
<Text className={`text-base ${isDarkMode ? 'text-gray-300' : 'text-gray-600'} text-center p-5`}>
Sie haben keine Berechtigung, Teams zu erstellen, oder Sie gehören keiner Organisation an.
</Text>
</View>
);
}
return (
<ScrollView className="flex-1">
<View className={`${isDarkMode ? 'bg-gray-800' : 'bg-white'} rounded-lg p-5 m-2.5 shadow`}>
<Text className={`text-2xl font-bold mb-5 ${isDarkMode ? 'text-gray-100' : 'text-gray-800'}`}>Neues Team erstellen</Text>
<View className="mb-4">
<Text className={`text-base font-medium mb-2 ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}>Organisation</Text>
<View className="flex-row flex-wrap mb-2.5">
{organizations.map((org) => (
<TouchableOpacity
key={org.id}
className={`${selectedOrgId === org.id ? (isDarkMode ? 'bg-blue-800' : 'bg-blue-600') : (isDarkMode ? 'bg-gray-700' : 'bg-gray-100')} py-2.5 px-4 rounded-lg mr-2.5 mb-2.5`}
onPress={() => setSelectedOrgId(org.id)}
>
<Text
className={`${selectedOrgId === org.id ? 'text-white' : (isDarkMode ? 'text-gray-300' : 'text-gray-700')} font-medium`}
>
{org.name}
</Text>
</TouchableOpacity>
))}
</View>
</View>
<View className="mb-4">
<Text className={`text-base font-medium mb-2 ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}>Teamname</Text>
<TextInput
className={`h-12 border ${isDarkMode ? 'border-gray-600 bg-gray-700 text-white' : 'border-gray-300 bg-gray-50 text-gray-800'} rounded-lg px-4 text-base`}
value={teamName}
onChangeText={setTeamName}
placeholder="Name des Teams eingeben"
placeholderTextColor={isDarkMode ? '#9CA3AF' : '#9CA3AF'}
/>
</View>
<View className="mb-4">
<Text className={`text-base font-medium mb-2 ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}>Zugewiesene Kredite</Text>
<TextInput
className={`h-12 border ${isDarkMode ? 'border-gray-600 bg-gray-700 text-white' : 'border-gray-300 bg-gray-50 text-gray-800'} rounded-lg px-4 text-base`}
value={allocatedCredits}
onChangeText={setAllocatedCredits}
placeholder="Anzahl der Kredite eingeben"
placeholderTextColor={isDarkMode ? '#9CA3AF' : '#9CA3AF'}
keyboardType="number-pad"
/>
</View>
<TouchableOpacity
className={`h-12 rounded-lg justify-center items-center mt-2.5 ${loading ? (isDarkMode ? 'bg-gray-600' : 'bg-gray-400') : (isDarkMode ? 'bg-blue-700' : 'bg-blue-600')}`}
onPress={createTeam}
disabled={loading}
>
{loading ? (
<ActivityIndicator color="#fff" size="small" />
) : (
<Text className="text-white text-base font-bold">Team erstellen</Text>
)}
</TouchableOpacity>
</View>
</ScrollView>
);
}
// NativeWind wird für das Styling verwendet, daher sind keine StyleSheet-Definitionen erforderlich

View file

@ -0,0 +1,165 @@
import React, { useState, useEffect } from 'react';
import { View, Text, TouchableOpacity, ActivityIndicator } from 'react-native';
import { FontAwesome5 } from '@expo/vector-icons';
import { useRouter } from 'expo-router';
import { supabase } from '../utils/supabase';
import { Session } from '@supabase/supabase-js';
import { useTheme, lightColors, darkColors } from '../utils/themeContext';
export default function DashboardStats() {
const router = useRouter();
const { isDarkMode } = useTheme();
const [session, setSession] = useState<Session | null>(null);
const [loading, setLoading] = useState(true);
const [teamCount, setTeamCount] = useState(0);
const [orgCount, setOrgCount] = useState(0);
const [availableMana, setAvailableMana] = useState(0);
const [totalMana, setTotalMana] = useState(0);
useEffect(() => {
// Prüfe den aktuellen Authentifizierungsstatus
supabase.auth.getSession().then(({ data: { session } }) => {
setSession(session);
if (session) {
fetchUserStats(session.user.id);
} else {
setLoading(false);
}
});
// Abonniere Authentifizierungsänderungen
const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => {
setSession(session);
if (session) {
fetchUserStats(session.user.id);
} else {
setLoading(false);
}
});
return () => subscription.unsubscribe();
}, []);
async function fetchUserStats(userId: string) {
try {
setLoading(true);
// Hole alle Teams, in denen der Benutzer Mitglied ist
const { data: teamMembers, error: teamMembersError } = await supabase
.from('team_members')
.select('team_id')
.eq('user_id', userId);
if (teamMembersError) throw teamMembersError;
setTeamCount(teamMembers?.length || 0);
// Hole alle Organisationen, in denen der Benutzer eine Rolle hat
const { data: userRoles, error: userRolesError } = await supabase
.from('user_roles')
.select('organization_id')
.eq('user_id', userId)
.not('organization_id', 'is', null);
if (userRolesError) throw userRolesError;
// Entferne Duplikate (falls der Benutzer mehrere Rollen in einer Organisation hat)
const uniqueOrgIds = [...new Set(userRoles?.map(role => role.organization_id) || [])];
setOrgCount(uniqueOrgIds.length);
// Hole die Mana-Informationen aus dem Profil des Benutzers
const { data: profileData, error: profileError } = await supabase
.from('profiles')
.select('individual_quota, individual_usage')
.eq('id', userId)
.single();
if (profileError) throw profileError;
if (profileData) {
const quota = profileData.individual_quota || 0;
const usage = profileData.individual_usage || 0;
const available = Math.max(0, quota - usage);
setTotalMana(quota);
setAvailableMana(available);
}
} catch (error) {
console.error('Fehler beim Abrufen der Benutzerstatistiken:', error);
} finally {
setLoading(false);
}
}
if (!session || loading) {
return (
<View className={`flex-row justify-between ${isDarkMode ? 'bg-gray-800' : 'bg-white'} rounded-lg p-4 mb-5 shadow`}>
<ActivityIndicator size="small" color={isDarkMode ? '#60A5FA' : '#0055FF'} />
</View>
);
}
return (
<View className="mb-5">
{/* Mana-Anzeige */}
<View className={`${isDarkMode ? 'bg-gray-800' : 'bg-white'} rounded-lg p-4 mb-3 shadow`}>
<TouchableOpacity
className="items-center"
onPress={() => router.push('/get-mana')}
>
<View className="flex-row items-center mb-2">
<FontAwesome5 name="fire" size={18} color="#F59E0B" className="mr-2" />
<Text className={`text-lg font-bold ${isDarkMode ? 'text-gray-100' : 'text-gray-800'}`}>Verfügbares Mana</Text>
</View>
<View className="w-full bg-gray-200 rounded-full h-4 mb-2 dark:bg-gray-700">
<View
className="h-4 rounded-full"
style={{
width: totalMana > 0 ? `${Math.min(100, (availableMana / totalMana) * 100)}%` : '0%',
backgroundColor: isDarkMode ? darkColors.primary : lightColors.primary
}}
/>
</View>
<View className="flex-row justify-between w-full">
<Text className={`text-sm ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`}>Verfügbar: {availableMana}</Text>
<Text className={`text-sm ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`}>Gesamt: {totalMana}</Text>
</View>
</TouchableOpacity>
</View>
{/* Teams und Organisationen */}
<View className={`flex-row justify-between ${isDarkMode ? 'bg-gray-800' : 'bg-white'} rounded-lg p-4 shadow`}>
<TouchableOpacity
className={`flex-1 flex-row items-center justify-center ${isDarkMode ? 'bg-gray-700' : 'bg-gray-50'} rounded-lg p-3 mr-2`}
onPress={() => router.push('/teams')}
>
<View className="items-center">
<View className="flex-row items-center mb-1">
<FontAwesome5 name="users" size={16} color={isDarkMode ? '#60A5FA' : '#0055FF'} className="mr-2" />
<Text className={`text-base font-semibold ${isDarkMode ? 'text-gray-100' : 'text-gray-800'}`}>Teams</Text>
</View>
<View className={`${isDarkMode ? 'bg-blue-900' : 'bg-blue-100'} px-3 py-1 rounded-full`}>
<Text className={`${isDarkMode ? 'text-blue-300' : 'text-blue-700'} font-medium`}>{teamCount}</Text>
</View>
</View>
</TouchableOpacity>
<TouchableOpacity
className={`flex-1 flex-row items-center justify-center ${isDarkMode ? 'bg-gray-700' : 'bg-gray-50'} rounded-lg p-3 ml-2`}
onPress={() => router.push('/organizations')}
>
<View className="items-center">
<View className="flex-row items-center mb-1">
<FontAwesome5 name="building" size={16} color={isDarkMode ? '#60A5FA' : '#0055FF'} className="mr-2" />
<Text className={`text-base font-semibold ${isDarkMode ? 'text-gray-100' : 'text-gray-800'}`}>Organisationen</Text>
</View>
<View className={`${isDarkMode ? 'bg-blue-900' : 'bg-blue-100'} px-3 py-1 rounded-full`}>
<Text className={`${isDarkMode ? 'text-blue-300' : 'text-blue-700'} font-medium`}>{orgCount}</Text>
</View>
</View>
</TouchableOpacity>
</View>
</View>
);
}

View file

@ -0,0 +1,29 @@
import { Text, View } from 'react-native';
export const EditScreenInfo = ({ path }: { path: string }) => {
const title = 'Open up the code for this screen:';
const description =
'Change any of the text, save the file, and your app will automatically update.';
return (
<View>
<View className={styles.getStartedContainer}>
<Text className={styles.getStartedText}>{title}</Text>
<View className={styles.codeHighlightContainer + styles.homeScreenFilename}>
<Text>{path}</Text>
</View>
<Text className={styles.getStartedText}>{description}</Text>
</View>
</View>
);
};
const styles = {
codeHighlightContainer: `rounded-md px-1`,
getStartedContainer: `items-center mx-12`,
getStartedText: `text-lg leading-6 text-center`,
helpContainer: `items-center mx-5 mt-4`,
helpLink: `py-4`,
helpLinkText: `text-center`,
homeScreenFilename: `my-2`,
};

View file

@ -0,0 +1,31 @@
import FontAwesome from '@expo/vector-icons/FontAwesome';
import { forwardRef } from 'react';
import { Pressable, StyleSheet } from 'react-native';
export const HeaderButton = forwardRef<typeof Pressable, { onPress?: () => void }>(
({ onPress }, ref) => {
return (
<Pressable onPress={onPress}>
{({ pressed }) => (
<FontAwesome
name="info-circle"
size={25}
color="gray"
style={[
styles.headerRight,
{
opacity: pressed ? 0.5 : 1,
},
]}
/>
)}
</Pressable>
);
}
);
export const styles = StyleSheet.create({
headerRight: {
marginRight: 15,
},
});

View file

@ -0,0 +1,304 @@
import React, { useState, useEffect, forwardRef, useImperativeHandle } from 'react';
import { View, Text, FlatList, TouchableOpacity, ActivityIndicator, Alert } from 'react-native';
import { FontAwesome5 } from '@expo/vector-icons';
import { supabase } from '../utils/supabase';
import { Session } from '@supabase/supabase-js';
import { useTheme } from '../utils/themeContext';
import { useRouter } from 'expo-router';
interface Organization {
id: string;
name: string;
total_credits: number;
used_credits: number;
created_at: string;
team_count?: number;
user_role?: string;
}
interface OrganizationListProps {
hideTitle?: boolean;
}
// Definiere die Ref-Schnittstelle
interface OrganizationListRef {
refreshOrganizations: () => void;
}
const OrganizationList = forwardRef<OrganizationListRef, OrganizationListProps>(({ hideTitle = false }, ref) => {
const [session, setSession] = useState<Session | null>(null);
const [loading, setLoading] = useState(true);
const [organizations, setOrganizations] = useState<Organization[]>([]);
const { isDarkMode } = useTheme();
const router = useRouter();
// Stelle die refreshOrganizations-Methode über die Ref zur Verfügung
useImperativeHandle(ref, () => ({
refreshOrganizations: () => {
if (session) {
console.log('Aktualisiere Organisationsliste');
fetchUserOrganizations(session.user.id);
}
}
}));
useEffect(() => {
// Prüfe den aktuellen Authentifizierungsstatus
supabase.auth.getSession().then(({ data: { session } }) => {
setSession(session);
if (session) {
fetchUserOrganizations(session.user.id);
} else {
setLoading(false);
}
});
// Abonniere Authentifizierungsänderungen
const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => {
setSession(session);
if (session) {
fetchUserOrganizations(session.user.id);
} else {
setLoading(false);
}
});
return () => subscription.unsubscribe();
}, []);
async function fetchUserOrganizations(userId: string) {
try {
setLoading(true);
// Hole alle Organisationen, in denen der Benutzer eine Rolle hat
const { data: userRoles, error: userRolesError } = await supabase
.from('user_roles')
.select('organization_id, role_id, roles(name)')
.eq('user_id', userId)
.not('organization_id', 'is', null) as {
data: Array<{
organization_id: string;
role_id: string;
roles: { name: string }
}> | null;
error: any
};
if (userRolesError) throw userRolesError;
if (userRoles && userRoles.length > 0) {
// Extrahiere die Organisations-IDs
const orgIds = [...new Set(userRoles.map(role => role.organization_id))];
// Hole die Organisations-Details
const { data: orgsData, error: orgsError } = await supabase
.from('organizations')
.select('id, name, total_credits, used_credits, created_at')
.in('id', orgIds);
if (orgsError) throw orgsError;
if (orgsData && orgsData.length > 0) {
// Hole die Anzahl der Teams für jede Organisation
const orgsWithTeamCount = await Promise.all(
orgsData.map(async (org) => {
// Finde die Rolle des Benutzers in dieser Organisation
const userRolesInOrg = userRoles.filter(role => role.organization_id === org.id);
const highestRole = findHighestRole(userRolesInOrg);
// Hole die Anzahl der Teams in dieser Organisation
const { count, error: teamCountError } = await supabase
.from('teams')
.select('id', { count: 'exact', head: true })
.eq('organization_id', org.id);
if (teamCountError) {
console.error('Fehler beim Abrufen der Team-Anzahl:', teamCountError);
return {
...org,
team_count: 0,
user_role: highestRole
};
}
return {
...org,
team_count: count || 0,
user_role: highestRole
};
})
);
setOrganizations(orgsWithTeamCount);
} else {
setOrganizations([]);
}
} else {
setOrganizations([]);
}
} catch (error) {
console.error('Fehler beim Abrufen der Organisationen:', error);
Alert.alert('Fehler', 'Es ist ein Fehler beim Laden Ihrer Organisationen aufgetreten.');
} finally {
setLoading(false);
}
}
// Hilfsfunktion, um die höchste Rolle eines Benutzers zu finden
function findHighestRole(userRoles: Array<{ role_id: string; roles: { name: string } }>): string {
const roleHierarchy = {
'system_admin': 4,
'org_admin': 3,
'team_admin': 2,
'member': 1
};
let highestRoleName = 'member';
let highestRoleValue = 0;
userRoles.forEach(role => {
const roleName = role.roles?.name;
if (roleName && roleHierarchy[roleName as keyof typeof roleHierarchy] > highestRoleValue) {
highestRoleName = roleName;
highestRoleValue = roleHierarchy[roleName as keyof typeof roleHierarchy];
}
});
return highestRoleName;
}
// Keine Aufklappfunktion mehr benötigt
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleDateString('de-DE', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
};
// Diese Funktion wird nicht mehr benötigt, da wir direkt mit NativeWind-Klassen arbeiten
const getRoleName = (role: string) => {
switch (role) {
case 'system_admin':
return 'System-Admin';
case 'org_admin':
return 'Organisations-Admin';
case 'team_admin':
return 'Team-Admin';
default:
return 'Mitglied';
}
};
if (!session) {
return (
<View className={`flex-1 ${isDarkMode ? 'bg-gray-800' : 'bg-white'} rounded-lg p-4 m-2.5 shadow`}>
<Text className={`text-base ${isDarkMode ? 'text-gray-300' : 'text-gray-600'} text-center p-5`}>
Bitte melden Sie sich an, um Ihre Organisationen zu sehen.
</Text>
</View>
);
}
if (loading) {
return (
<View className={`flex-1 ${isDarkMode ? 'bg-gray-800' : 'bg-white'} rounded-lg p-4 m-2.5 shadow`}>
<ActivityIndicator size="large" color={isDarkMode ? '#93C5FD' : '#0055FF'} />
<Text className={`text-base ${isDarkMode ? 'text-gray-300' : 'text-gray-600'} text-center mt-2.5`}>Lade Organisationen...</Text>
</View>
);
}
if (organizations.length === 0) {
return (
<View className={`flex-1 ${isDarkMode ? 'bg-gray-800' : 'bg-white'} rounded-lg p-4 m-2.5 shadow`}>
<FontAwesome5 name="building" size={50} color={isDarkMode ? '#4B5563' : '#ccc'} className="self-center mb-4" />
<Text className={`text-lg font-medium ${isDarkMode ? 'text-gray-100' : 'text-gray-800'} text-center mb-2.5`}>
Sie gehören derzeit keiner Organisation an.
</Text>
<Text className={`text-sm ${isDarkMode ? 'text-gray-300' : 'text-gray-600'} text-center px-5`}>
Erstellen Sie eine neue Organisation oder bitten Sie einen Administrator, Sie einer Organisation hinzuzufügen.
</Text>
</View>
);
}
return (
<View className={`flex-1 ${isDarkMode ? 'bg-gray-800' : 'bg-white'} rounded-lg p-4 m-2.5 shadow`}>
{!hideTitle && (
<Text className={`text-2xl font-bold mb-4 ${isDarkMode ? 'text-gray-100' : 'text-gray-800'}`}>Meine Organisationen</Text>
)}
<FlatList
data={organizations}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<TouchableOpacity
onPress={() => {
console.log('Organisation angeklickt:', item.id, item.name);
router.push({
pathname: '/organizations/[id]',
params: { id: item.id, name: item.name }
});
}}
className={`${isDarkMode ? 'bg-gray-700' : 'bg-gray-50'} rounded-lg p-4 mb-2.5 shadow-sm`}
activeOpacity={0.7}
>
{/* Header mit Organisationsname */}
<View className="flex-row justify-between items-center mb-3">
<View className="flex-row items-center">
<FontAwesome5 name="building" size={18} color={isDarkMode ? '#93C5FD' : '#0055FF'} className="mr-2.5" />
<Text className={`text-lg font-semibold ${isDarkMode ? 'text-gray-100' : 'text-gray-800'}`}>{item.name}</Text>
</View>
<View className={`px-2 py-0.5 rounded-full ${item.user_role === 'system_admin' ? 'bg-red-600' : item.user_role === 'org_admin' ? 'bg-orange-500' : item.user_role === 'team_admin' ? 'bg-green-600' : 'bg-gray-600'}`}>
<Text className="text-xs font-medium text-white">{getRoleName(item.user_role || 'member')}</Text>
</View>
</View>
{/* Mittlerer Bereich mit Teams und Erstellungsdatum */}
<View className="flex-row justify-between items-center mb-3">
<View className="flex-row items-center">
<FontAwesome5 name="users" size={14} color={isDarkMode ? '#9CA3AF' : '#666'} className="mr-2" />
<Text className={`text-sm ${isDarkMode ? 'text-gray-300' : 'text-gray-600'}`}>{item.team_count} {item.team_count === 1 ? 'Team' : 'Teams'}</Text>
</View>
<View className="flex-row items-center">
<FontAwesome5 name="calendar-alt" size={14} color={isDarkMode ? '#9CA3AF' : '#666'} className="mr-2" />
<Text className={`text-sm ${isDarkMode ? 'text-gray-300' : 'text-gray-600'}`}>{formatDate(item.created_at)}</Text>
</View>
</View>
{/* Unterer Bereich mit Kreditinformationen */}
<View className={`flex-row justify-between mt-1 pt-2 border-t ${isDarkMode ? 'border-gray-600' : 'border-gray-200'}`}>
<View className="flex-1 items-center">
<Text className={`text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-600'} mb-1`}>Gesamte Kredite</Text>
<Text className={`text-base font-semibold ${isDarkMode ? 'text-gray-100' : 'text-gray-800'}`}>{item.total_credits}</Text>
</View>
<View className={`flex-1 items-center border-x ${isDarkMode ? 'border-gray-600' : 'border-gray-200'}`}>
<Text className={`text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-600'} mb-1`}>Verwendete Kredite</Text>
<Text className={`text-base font-semibold ${isDarkMode ? 'text-gray-100' : 'text-gray-800'}`}>{item.used_credits}</Text>
</View>
<View className="flex-1 items-center">
<Text className={`text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-600'} mb-1`}>Verfügbar</Text>
<Text
className={`text-base font-semibold ${item.total_credits - item.used_credits > 0 ? (isDarkMode ? 'text-green-400' : 'text-green-600') : (isDarkMode ? 'text-red-400' : 'text-red-600')}`}
>
{item.total_credits - item.used_credits}
</Text>
</View>
</View>
</TouchableOpacity>
)}
className="pb-5"
/>
</View>
);
});
// NativeWind wird für das Styling verwendet, daher sind keine StyleSheet-Definitionen erforderlich
// Exportiere die Komponente mit forwardRef
export default OrganizationList;

View file

@ -0,0 +1,25 @@
import { Text, View } from 'react-native';
import { EditScreenInfo } from './EditScreenInfo';
type ScreenContentProps = {
title: string;
path: string;
children?: React.ReactNode;
};
export const ScreenContent = ({ title, path, children }: ScreenContentProps) => {
return (
<View className={styles.container}>
<Text className={styles.title}>{title}</Text>
<View className={styles.separator} />
<EditScreenInfo path={path} />
{children}
</View>
);
};
const styles = {
container: `items-center flex-1 justify-center`,
separator: `h-[1px] my-7 w-4/5 bg-gray-200`,
title: `text-xl font-bold`,
};

View file

@ -0,0 +1,593 @@
import React, { useState, useEffect } from 'react';
import { View, Text, TextInput, TouchableOpacity, StyleSheet, Alert, ActivityIndicator } from 'react-native';
import { supabase } from '../utils/supabase';
import { Session } from '@supabase/supabase-js';
import { useTheme } from '../utils/themeContext';
interface User {
id: string;
email: string;
first_name?: string;
last_name?: string;
}
export default function SendMana() {
const { isDarkMode } = useTheme();
const [session, setSession] = useState<Session | null>(null);
const [loading, setLoading] = useState(false);
const [searchLoading, setSearchLoading] = useState(false);
const [recipientEmail, setRecipientEmail] = useState('');
const [manaAmount, setManaAmount] = useState('');
const [description, setDescription] = useState('');
const [foundUser, setFoundUser] = useState<User | null>(null);
const [userCredits, setUserCredits] = useState(0);
useEffect(() => {
// Prüfe den aktuellen Authentifizierungsstatus
supabase.auth.getSession().then(({ data: { session } }) => {
setSession(session);
if (session) {
fetchUserCredits(session.user.id);
}
});
// Abonniere Authentifizierungsänderungen
const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => {
setSession(session);
if (session) {
fetchUserCredits(session.user.id);
}
});
return () => subscription.unsubscribe();
}, []);
async function fetchUserCredits(userId: string) {
try {
// Zuerst prüfen wir, ob der Benutzer ein Einzelnutzer ist
const { data: profileData, error: profileError } = await supabase
.from('profiles')
.select('is_individual, individual_quota, individual_usage')
.eq('id', userId)
.single();
if (profileError) throw profileError;
if (profileData && profileData.is_individual) {
// Wenn der Benutzer ein Einzelnutzer ist, verwenden wir die individuellen Kredite
setUserCredits(profileData.individual_quota - profileData.individual_usage);
return;
}
// Wenn der Benutzer kein Einzelnutzer ist, prüfen wir die Teamzugehörigkeit
const { data: teamMemberData, error: teamMemberError } = await supabase
.from('team_members')
.select('allocated_credits, used_credits')
.eq('user_id', userId);
if (teamMemberError) throw teamMemberError;
if (teamMemberData && teamMemberData.length > 0) {
// Berechne die Summe der verfügbaren Kredite aus allen Teams
const availableCredits = teamMemberData.reduce((total, member) => {
return total + (member.allocated_credits - member.used_credits);
}, 0);
setUserCredits(availableCredits);
} else {
setUserCredits(0);
}
} catch (error) {
console.error('Fehler beim Abrufen der Kredite:', error);
setUserCredits(0);
}
}
async function searchUser() {
if (!recipientEmail.trim()) {
Alert.alert('Fehler', 'Bitte geben Sie eine E-Mail-Adresse ein.');
return;
}
try {
setSearchLoading(true);
// In einer realen Anwendung würden wir hier eine sichere Methode verwenden,
// um Benutzer anhand ihrer E-Mail-Adresse zu finden, z.B. über eine spezielle API
// oder eine Supabase-Funktion. Für dieses Beispiel simulieren wir die Suche.
// Suche nach dem Profil mit einer bestimmten E-Mail
// Hinweis: In einer echten Anwendung würde dies über eine sichere API erfolgen
const { data: profiles, error: profileError } = await supabase
.from('profiles')
.select('id, first_name, last_name')
.limit(1);
if (profileError || !profiles || profiles.length === 0) {
Alert.alert('Fehler', 'Benutzer nicht gefunden.');
setFoundUser(null);
return;
}
// Für Demonstrationszwecke verwenden wir das erste gefundene Profil
// In einer realen Anwendung würden wir natürlich das richtige Profil finden
const profile = profiles[0];
setFoundUser({
id: profile.id,
email: recipientEmail.trim(),
first_name: profile.first_name || undefined,
last_name: profile.last_name || undefined
});
} catch (error) {
console.error('Fehler bei der Benutzersuche:', error);
Alert.alert('Fehler', 'Es ist ein Fehler aufgetreten. Bitte versuchen Sie es später erneut.');
} finally {
setSearchLoading(false);
}
}
async function sendMana() {
if (!session) {
Alert.alert('Fehler', 'Sie müssen angemeldet sein, um Mana zu senden.');
return;
}
if (!foundUser) {
Alert.alert('Fehler', 'Bitte suchen Sie zuerst nach einem Empfänger.');
return;
}
const amount = parseInt(manaAmount);
if (isNaN(amount) || amount <= 0) {
Alert.alert('Fehler', 'Bitte geben Sie einen gültigen Mana-Betrag ein.');
return;
}
if (amount > userCredits) {
Alert.alert('Fehler', 'Sie haben nicht genügend Mana-Kredite.');
return;
}
try {
setLoading(true);
// 1. Erstelle einen Eintrag in der credit_transactions Tabelle
const { error: transactionError } = await supabase
.from('credit_transactions')
.insert([
{
user_id: foundUser.id,
amount: amount,
description: description || 'Mana-Übertragung',
app_id: 'manacore'
}
]);
if (transactionError) throw transactionError;
// 2. Aktualisiere die Kredite des Empfängers
const { data: recipientProfile, error: recipientProfileError } = await supabase
.from('profiles')
.select('is_individual, individual_quota')
.eq('id', foundUser.id)
.single();
if (recipientProfileError) throw recipientProfileError;
if (recipientProfile && recipientProfile.is_individual) {
// Wenn der Empfänger ein Einzelnutzer ist, erhöhen wir sein Kontingent
const { error: updateError } = await supabase
.from('profiles')
.update({
individual_quota: recipientProfile.individual_quota + amount
})
.eq('id', foundUser.id);
if (updateError) throw updateError;
}
// 3. Aktualisiere die Kredite des Senders
const { data: senderProfile, error: senderProfileError } = await supabase
.from('profiles')
.select('is_individual, individual_usage')
.eq('id', session.user.id)
.single();
if (senderProfileError) throw senderProfileError;
if (senderProfile && senderProfile.is_individual) {
// Wenn der Sender ein Einzelnutzer ist, erhöhen wir seine Nutzung
const { error: updateError } = await supabase
.from('profiles')
.update({
individual_usage: senderProfile.individual_usage + amount
})
.eq('id', session.user.id);
if (updateError) throw updateError;
} else {
// Wenn der Sender kein Einzelnutzer ist, müssen wir prüfen, ob er zu einem Team gehört
// und die entsprechenden Kredite aktualisieren
// Diese Logik würde komplexer sein und hängt von Ihrer spezifischen Implementierung ab
}
Alert.alert('Erfolg', `${amount} Mana wurden erfolgreich an ${foundUser.email} gesendet.`);
// Formular zurücksetzen
setRecipientEmail('');
setManaAmount('');
setDescription('');
setFoundUser(null);
// Aktualisiere die Kredite des Benutzers
fetchUserCredits(session.user.id);
} catch (error) {
console.error('Fehler beim Senden von Mana:', error);
Alert.alert('Fehler', 'Es ist ein Fehler aufgetreten. Bitte versuchen Sie es später erneut.');
} finally {
setLoading(false);
}
}
// Dynamische Stile basierend auf dem aktuellen Theme
const styles = StyleSheet.create({
container: {
backgroundColor: isDarkMode ? '#1E1E1E' : 'white',
borderRadius: 10,
padding: 20,
marginBottom: 20,
shadowColor: isDarkMode ? '#000000' : '#000000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: isDarkMode ? 0.5 : 0.1,
shadowRadius: 4,
elevation: 3,
},
header: {
fontSize: 22,
fontWeight: 'bold',
marginBottom: 20,
color: isDarkMode ? '#F9FAFB' : '#333',
},
creditInfo: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: isDarkMode ? '#1E3A8A' : '#f0f8ff',
padding: 15,
borderRadius: 8,
marginBottom: 20,
},
creditLabel: {
fontSize: 16,
fontWeight: '500',
color: isDarkMode ? '#E5E7EB' : '#333',
},
creditAmount: {
fontSize: 18,
fontWeight: 'bold',
color: isDarkMode ? '#93C5FD' : '#0055FF',
marginLeft: 10,
},
formGroup: {
marginBottom: 15,
},
label: {
fontSize: 16,
marginBottom: 8,
fontWeight: '500',
color: isDarkMode ? '#E5E7EB' : '#333',
},
input: {
height: 50,
borderWidth: 1,
borderColor: isDarkMode ? '#4B5563' : '#ddd',
borderRadius: 8,
paddingHorizontal: 15,
backgroundColor: isDarkMode ? '#374151' : '#f9f9f9',
fontSize: 16,
color: isDarkMode ? '#F9FAFB' : '#333',
},
searchContainer: {
flexDirection: 'row',
alignItems: 'center',
},
searchInput: {
flex: 1,
height: 50,
borderWidth: 1,
borderColor: isDarkMode ? '#4B5563' : '#ddd',
borderRadius: 8,
paddingHorizontal: 15,
backgroundColor: isDarkMode ? '#374151' : '#f9f9f9',
fontSize: 16,
color: isDarkMode ? '#F9FAFB' : '#333',
},
searchButton: {
backgroundColor: isDarkMode ? '#3B82F6' : '#0055FF',
height: 50,
paddingHorizontal: 15,
borderRadius: 8,
justifyContent: 'center',
alignItems: 'center',
marginLeft: 10,
},
searchButtonText: {
color: 'white',
fontSize: 14,
fontWeight: 'bold',
},
userCard: {
backgroundColor: isDarkMode ? '#1E3A8A' : '#f0f8ff',
padding: 15,
borderRadius: 8,
marginBottom: 20,
borderLeftWidth: 4,
borderLeftColor: isDarkMode ? '#3B82F6' : '#0055FF',
},
userCardTitle: {
fontSize: 14,
color: isDarkMode ? '#9CA3AF' : '#666',
marginBottom: 5,
},
userCardEmail: {
fontSize: 16,
fontWeight: 'bold',
color: isDarkMode ? '#F9FAFB' : '#333',
},
userCardName: {
fontSize: 14,
color: isDarkMode ? '#9CA3AF' : '#666',
marginTop: 5,
},
sendButton: {
backgroundColor: isDarkMode ? '#3B82F6' : '#0055FF',
height: 50,
borderRadius: 8,
justifyContent: 'center',
alignItems: 'center',
marginTop: 10,
},
sendButtonText: {
color: 'white',
fontSize: 16,
fontWeight: 'bold',
},
disabledButton: {
backgroundColor: isDarkMode ? '#4B5563' : '#ccc',
},
notLoggedIn: {
fontSize: 16,
color: isDarkMode ? '#9CA3AF' : '#666',
textAlign: 'center',
padding: 20,
},
});
if (!session) {
return (
<View style={styles.container}>
<Text style={styles.notLoggedIn}>
Bitte melden Sie sich an, um Mana zu senden.
</Text>
</View>
);
}
return (
<View style={styles.container}>
<Text style={styles.header}>Mana senden</Text>
<View style={styles.creditInfo}>
<Text style={styles.creditLabel}>Verfügbares Mana:</Text>
<Text style={styles.creditAmount}>{userCredits}</Text>
</View>
<View style={styles.formGroup}>
<Text style={styles.label}>Empfänger E-Mail</Text>
<View style={styles.searchContainer}>
<TextInput
style={styles.searchInput}
value={recipientEmail}
onChangeText={setRecipientEmail}
placeholder="E-Mail des Empfängers"
placeholderTextColor={isDarkMode ? '#9CA3AF' : '#9CA3AF'}
autoCapitalize="none"
keyboardType="email-address"
/>
<TouchableOpacity
style={styles.searchButton}
onPress={searchUser}
disabled={searchLoading}
>
{searchLoading ? (
<ActivityIndicator color="#fff" size="small" />
) : (
<Text style={styles.searchButtonText}>Suchen</Text>
)}
</TouchableOpacity>
</View>
</View>
{foundUser && (
<View style={styles.userCard}>
<Text style={styles.userCardTitle}>Empfänger gefunden:</Text>
<Text style={styles.userCardEmail}>{foundUser.email}</Text>
{(foundUser.first_name || foundUser.last_name) && (
<Text style={styles.userCardName}>
{[foundUser.first_name, foundUser.last_name].filter(Boolean).join(' ')}
</Text>
)}
</View>
)}
<View style={styles.formGroup}>
<Text style={styles.label}>Mana-Betrag</Text>
<TextInput
style={styles.input}
value={manaAmount}
onChangeText={setManaAmount}
placeholder="Betrag eingeben"
placeholderTextColor={isDarkMode ? '#9CA3AF' : '#9CA3AF'}
keyboardType="number-pad"
/>
</View>
<View style={styles.formGroup}>
<Text style={styles.label}>Beschreibung (optional)</Text>
<TextInput
style={styles.input}
value={description}
onChangeText={setDescription}
placeholder="Grund für die Übertragung"
placeholderTextColor={isDarkMode ? '#9CA3AF' : '#9CA3AF'}
multiline
/>
</View>
<TouchableOpacity
style={[
styles.sendButton,
(!foundUser || loading) && styles.disabledButton
]}
onPress={sendMana}
disabled={!foundUser || loading}
>
{loading ? (
<ActivityIndicator color="#fff" size="small" />
) : (
<Text style={styles.sendButtonText}>Mana senden</Text>
)}
</TouchableOpacity>
</View>
);
}
const styles = StyleSheet.create({
container: {
backgroundColor: 'white',
borderRadius: 10,
padding: 20,
marginBottom: 20,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
header: {
fontSize: 22,
fontWeight: 'bold',
marginBottom: 20,
color: '#333',
},
creditInfo: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#f0f8ff',
padding: 15,
borderRadius: 8,
marginBottom: 20,
},
creditLabel: {
fontSize: 16,
fontWeight: '500',
color: '#333',
},
creditAmount: {
fontSize: 18,
fontWeight: 'bold',
color: '#0055FF',
marginLeft: 10,
},
formGroup: {
marginBottom: 15,
},
label: {
fontSize: 16,
marginBottom: 8,
fontWeight: '500',
color: '#333',
},
input: {
height: 50,
borderWidth: 1,
borderColor: '#ddd',
borderRadius: 8,
paddingHorizontal: 15,
backgroundColor: '#f9f9f9',
fontSize: 16,
},
searchContainer: {
flexDirection: 'row',
alignItems: 'center',
},
searchInput: {
flex: 1,
height: 50,
borderWidth: 1,
borderColor: '#ddd',
borderRadius: 8,
paddingHorizontal: 15,
backgroundColor: '#f9f9f9',
fontSize: 16,
},
searchButton: {
backgroundColor: '#0055FF',
height: 50,
paddingHorizontal: 15,
borderRadius: 8,
justifyContent: 'center',
alignItems: 'center',
marginLeft: 10,
},
searchButtonText: {
color: 'white',
fontSize: 14,
fontWeight: 'bold',
},
userCard: {
backgroundColor: '#f0f8ff',
padding: 15,
borderRadius: 8,
marginBottom: 20,
borderLeftWidth: 4,
borderLeftColor: '#0055FF',
},
userCardTitle: {
fontSize: 14,
color: '#666',
marginBottom: 5,
},
userCardEmail: {
fontSize: 16,
fontWeight: 'bold',
color: '#333',
},
userCardName: {
fontSize: 14,
color: '#666',
marginTop: 5,
},
sendButton: {
backgroundColor: '#0055FF',
height: 50,
borderRadius: 8,
justifyContent: 'center',
alignItems: 'center',
marginTop: 10,
},
sendButtonText: {
color: 'white',
fontSize: 16,
fontWeight: 'bold',
},
disabledButton: {
backgroundColor: '#ccc',
},
notLoggedIn: {
fontSize: 16,
color: '#666',
textAlign: 'center',
padding: 20,
},
});

View file

@ -0,0 +1,15 @@
import FontAwesome from '@expo/vector-icons/FontAwesome';
import { StyleSheet } from 'react-native';
export const TabBarIcon = (props: {
name: React.ComponentProps<typeof FontAwesome>['name'];
color: string;
}) => {
return <FontAwesome size={28} style={styles.tabBarIcon} {...props} />;
};
export const styles = StyleSheet.create({
tabBarIcon: {
marginBottom: -3,
},
});

View file

@ -0,0 +1,338 @@
import React, { useState, useEffect } from 'react';
import { View, Text, FlatList, TouchableOpacity, ActivityIndicator, Alert, Platform } from 'react-native';
import { FontAwesome5 } from '@expo/vector-icons';
import { supabase } from '../utils/supabase';
import { Session } from '@supabase/supabase-js';
import { useRouter } from 'expo-router';
import { useTheme } from '../utils/themeContext';
interface Team {
id: string;
name: string;
organization_id: string;
organization_name?: string;
allocated_credits?: number;
used_credits?: number;
created_at: string;
}
interface TeamListProps {
hideTitle?: boolean;
}
// Funktion zum Aktualisieren der Teamliste, die von außen aufgerufen werden kann
export function refreshTeamList(userId: string, callback?: () => void) {
if (!userId) return;
// Hole alle Teams, in denen der Benutzer Mitglied ist
supabase
.from('team_members')
.select('team_id, allocated_credits, used_credits')
.eq('user_id', userId)
.then(({ data: teamMembers, error: teamMembersError }) => {
if (teamMembersError) {
console.error('Fehler beim Abrufen der Team-Mitglieder:', teamMembersError);
if (callback) callback();
return;
}
if (teamMembers && teamMembers.length > 0) {
// Extrahiere die Team-IDs
const teamIds = teamMembers.map(member => member.team_id);
// Hole die Team-Details
supabase
.from('teams')
.select('id, name, organization_id, created_at')
.in('id', teamIds)
.then(({ data: teamsData, error: teamsError }) => {
if (teamsError) {
console.error('Fehler beim Abrufen der Teams:', teamsError);
if (callback) callback();
return;
}
if (teamsData && teamsData.length > 0) {
// Hole die Organisationsnamen für jedes Team
const orgIds = [...new Set(teamsData.map(team => team.organization_id))];
supabase
.from('organizations')
.select('id, name')
.in('id', orgIds)
.then(({ data: orgsData, error: orgsError }) => {
if (orgsError) {
console.error('Fehler beim Abrufen der Organisationen:', orgsError);
if (callback) callback();
return;
}
// Globales Event auslösen, um alle TeamList-Komponenten zu aktualisieren
const event = new CustomEvent('teamlist-refresh', {
detail: {
teams: teamsData.map(team => {
const org = orgsData?.find(org => org.id === team.organization_id);
const memberInfo = teamMembers.find(member => member.team_id === team.id);
return {
...team,
organization_name: org?.name || 'Unbekannte Organisation',
allocated_credits: memberInfo?.allocated_credits || 0,
used_credits: memberInfo?.used_credits || 0
};
})
}
});
// Event auslösen
if (typeof window !== 'undefined') {
window.dispatchEvent(event);
}
if (callback) callback();
});
} else {
// Globales Event mit leerer Liste auslösen
if (typeof window !== 'undefined') {
window.dispatchEvent(new CustomEvent('teamlist-refresh', { detail: { teams: [] } }));
}
if (callback) callback();
}
});
} else {
// Globales Event mit leerer Liste auslösen
if (typeof window !== 'undefined') {
window.dispatchEvent(new CustomEvent('teamlist-refresh', { detail: { teams: [] } }));
}
if (callback) callback();
}
});
}
export default function TeamList({ hideTitle = false }: TeamListProps) {
const router = useRouter();
const [session, setSession] = useState<Session | null>(null);
const [loading, setLoading] = useState(true);
const [teams, setTeams] = useState<Team[]>([]);
const { isDarkMode } = useTheme();
useEffect(() => {
// Prüfe den aktuellen Authentifizierungsstatus
supabase.auth.getSession().then(({ data: { session } }) => {
setSession(session);
if (session) {
fetchUserTeams(session.user.id);
} else {
setLoading(false);
}
});
// Abonniere Authentifizierungsänderungen
const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => {
setSession(session);
if (session) {
fetchUserTeams(session.user.id);
} else {
setLoading(false);
}
});
// Listener für teamlist-refresh Event hinzufügen
const handleTeamListRefresh = (event: any) => {
if (event.detail && event.detail.teams) {
setTeams(event.detail.teams);
setLoading(false);
}
};
if (typeof window !== 'undefined') {
window.addEventListener('teamlist-refresh', handleTeamListRefresh);
}
return () => {
subscription.unsubscribe();
if (typeof window !== 'undefined') {
window.removeEventListener('teamlist-refresh', handleTeamListRefresh);
}
};
}, []);
async function fetchUserTeams(userId: string) {
try {
setLoading(true);
// Hole alle Teams, in denen der Benutzer Mitglied ist
const { data: teamMembers, error: teamMembersError } = await supabase
.from('team_members')
.select('team_id, allocated_credits, used_credits')
.eq('user_id', userId);
if (teamMembersError) throw teamMembersError;
if (teamMembers && teamMembers.length > 0) {
// Extrahiere die Team-IDs
const teamIds = teamMembers.map(member => member.team_id);
// Hole die Team-Details
const { data: teamsData, error: teamsError } = await supabase
.from('teams')
.select('id, name, organization_id, created_at')
.in('id', teamIds);
if (teamsError) throw teamsError;
if (teamsData && teamsData.length > 0) {
// Hole die Organisationsnamen für jedes Team
const orgIds = [...new Set(teamsData.map(team => team.organization_id))];
const { data: orgsData, error: orgsError } = await supabase
.from('organizations')
.select('id, name')
.in('id', orgIds);
if (orgsError) throw orgsError;
// Füge Organisationsnamen und Kredit-Informationen zu den Teams hinzu
const enhancedTeams = teamsData.map(team => {
const org = orgsData?.find(org => org.id === team.organization_id);
const memberInfo = teamMembers.find(member => member.team_id === team.id);
return {
...team,
organization_name: org?.name || 'Unbekannte Organisation',
allocated_credits: memberInfo?.allocated_credits || 0,
used_credits: memberInfo?.used_credits || 0
};
});
setTeams(enhancedTeams);
} else {
setTeams([]);
}
} else {
setTeams([]);
}
} catch (error) {
console.error('Fehler beim Abrufen der Teams:', error);
Alert.alert('Fehler', 'Es ist ein Fehler beim Laden Ihrer Teams aufgetreten.');
} finally {
setLoading(false);
}
}
// Keine Aufklappfunktion mehr benötigt
const navigateToTeamDetails = (teamId: string, teamName: string) => {
// Verwende die korrekte Expo Router Navigation
router.push({
pathname: '/teams/[id]',
params: { id: teamId, name: teamName }
});
};
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleDateString('de-DE', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
};
if (!session) {
return (
<Text className={`text-base ${isDarkMode ? 'text-gray-300' : 'text-gray-600'} text-center p-5 mx-2.5`}>
Bitte melden Sie sich an, um Ihre Teams zu sehen.
</Text>
);
}
if (loading) {
return (
<View className="flex-1 items-center justify-center p-4 mx-2.5">
<ActivityIndicator size="large" color="#0055FF" />
<Text className={`text-base ${isDarkMode ? 'text-gray-300' : 'text-gray-600'} text-center mt-2.5`}>Lade Teams...</Text>
</View>
);
}
if (teams.length === 0) {
return (
<View className="flex-1 items-center justify-center p-4 mx-2.5">
<FontAwesome5 name="users-slash" size={50} color={isDarkMode ? '#4B5563' : '#ccc'} className="mb-4" />
<Text className={`text-lg font-medium ${isDarkMode ? 'text-gray-100' : 'text-gray-800'} text-center mb-2.5`}>
Sie sind derzeit kein Mitglied in einem Team.
</Text>
<Text className={`text-sm ${isDarkMode ? 'text-gray-300' : 'text-gray-600'} text-center px-5`}>
Erstellen Sie ein neues Team oder bitten Sie einen Administrator, Sie einem Team hinzuzufügen.
</Text>
</View>
);
}
return (
<>
{!hideTitle && (
<Text className={`text-2xl font-bold mb-4 ${isDarkMode ? 'text-gray-100' : 'text-gray-800'} px-2.5`}>Meine Teams</Text>
)}
<FlatList
data={teams}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<TouchableOpacity
className={`${isDarkMode ? 'bg-gray-700' : 'bg-gray-50'} rounded-lg p-4 mb-2.5 shadow-sm`}
style={Platform.OS === 'web' ? { cursor: 'pointer' } : undefined}
onPress={() => navigateToTeamDetails(item.id, item.name)}
activeOpacity={0.7}
>
{/* Header mit Teamname */}
<View className="flex-row justify-between items-center mb-3">
<View className="flex-row items-center">
<FontAwesome5 name="users" size={18} color={isDarkMode ? '#93C5FD' : '#0055FF'} className="mr-2.5" />
<Text className={`text-lg font-semibold ${isDarkMode ? 'text-gray-100' : 'text-gray-800'}`}>{item.name}</Text>
</View>
<FontAwesome5 name="chevron-right" size={14} color={isDarkMode ? '#93C5FD' : '#0055FF'} />
</View>
{/* Mittlerer Bereich mit Organisation und Erstellungsdatum */}
<View className="flex-row justify-between items-center mb-3">
<View className="flex-row items-center">
<FontAwesome5 name="building" size={14} color={isDarkMode ? '#9CA3AF' : '#666'} className="mr-2" />
<Text className={`text-sm ${isDarkMode ? 'text-gray-300' : 'text-gray-600'}`}>{item.organization_name}</Text>
</View>
<View className="flex-row items-center">
<FontAwesome5 name="calendar-alt" size={14} color={isDarkMode ? '#9CA3AF' : '#666'} className="mr-2" />
<Text className={`text-sm ${isDarkMode ? 'text-gray-300' : 'text-gray-600'}`}>{formatDate(item.created_at)}</Text>
</View>
</View>
{/* Unterer Bereich mit Kreditinformationen */}
<View className={`flex-row justify-between mt-1 pt-2 border-t ${isDarkMode ? 'border-gray-600' : 'border-gray-200'}`}>
<View className="flex-1 items-center">
<Text className={`text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-600'} mb-1`}>Zugewiesen</Text>
<Text className={`text-base font-semibold ${isDarkMode ? 'text-gray-100' : 'text-gray-800'}`}>{item.allocated_credits}</Text>
</View>
<View className={`flex-1 items-center border-x ${isDarkMode ? 'border-gray-600' : 'border-gray-200'}`}>
<Text className={`text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-600'} mb-1`}>Verwendet</Text>
<Text className={`text-base font-semibold ${isDarkMode ? 'text-gray-100' : 'text-gray-800'}`}>{item.used_credits}</Text>
</View>
<View className="flex-1 items-center">
<Text className={`text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-600'} mb-1`}>Verfügbar</Text>
<Text
className={`text-base font-semibold ${(item.allocated_credits || 0) - (item.used_credits || 0) > 0 ? (isDarkMode ? 'text-green-400' : 'text-green-600') : (isDarkMode ? 'text-red-400' : 'text-red-600')}`}
>
{(item.allocated_credits || 0) - (item.used_credits || 0)}
</Text>
</View>
</View>
</TouchableOpacity>
)}
className="pb-5 px-2.5"
/>
</>
);
}
// NativeWind wird für das Styling verwendet, daher sind keine StyleSheet-Definitionen erforderlich

View file

@ -0,0 +1,664 @@
import React, { useState, useEffect } from 'react';
import { View, Text, FlatList, TouchableOpacity, ActivityIndicator, Alert, TextInput } from 'react-native';
import { FontAwesome5 } from '@expo/vector-icons';
import { supabase } from '../utils/supabase';
import { Session } from '@supabase/supabase-js';
import { useLocalSearchParams } from 'expo-router';
import { useTheme } from '../utils/themeContext';
interface TeamMember {
// Anpassung an die tatsächliche Datenbankstruktur
user_id: string;
team_id: string;
allocated_credits: number;
used_credits: number;
first_name?: string;
last_name?: string;
created_at: string;
updated_at?: string;
}
interface TeamDetails {
id: string;
name: string;
organization_id: string;
organization_name?: string;
created_at: string;
}
interface TeamMembersProps {
teamId: string;
}
export default function TeamMembers({ teamId }: TeamMembersProps) {
const [session, setSession] = useState<Session | null>(null);
const [loading, setLoading] = useState(true);
const [teamDetails, setTeamDetails] = useState<TeamDetails | null>(null);
const [members, setMembers] = useState<TeamMember[]>([]);
const [newMemberEmail, setNewMemberEmail] = useState('');
const [inviting, setInviting] = useState(false);
const [userRole, setUserRole] = useState<string | null>(null);
const [isAdmin, setIsAdmin] = useState(false);
const { isDarkMode } = useTheme();
const [isEditing, setIsEditing] = useState(false);
const [newTeamName, setNewTeamName] = useState('');
const [updatingName, setUpdatingName] = useState(false);
// Zustandsvariablen für die Bearbeitung der Mitgliederlimits
const [editingMemberId, setEditingMemberId] = useState<string | null>(null);
const [newCreditLimit, setNewCreditLimit] = useState('');
const [updatingLimit, setUpdatingLimit] = useState(false);
useEffect(() => {
if (!teamId) {
Alert.alert('Fehler', 'Team-ID nicht gefunden');
return;
}
// Setze den neuen Teamnamen, wenn sich die Teamdetails ändern
if (teamDetails) {
setNewTeamName(teamDetails.name);
}
// Prüfe den aktuellen Authentifizierungsstatus
supabase.auth.getSession().then(({ data: { session } }) => {
setSession(session);
if (session) {
fetchTeamDetails(teamId);
fetchTeamMembers(teamId);
checkUserRole(session.user.id, teamId);
} else {
setLoading(false);
}
});
// Abonniere Authentifizierungsänderungen
const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => {
setSession(session);
if (session) {
fetchTeamDetails(teamId);
fetchTeamMembers(teamId);
checkUserRole(session.user.id, teamId);
} else {
setLoading(false);
}
});
return () => subscription.unsubscribe();
}, [teamId]);
async function checkUserRole(userId: string, teamId: string) {
try {
// Vereinfachte Version: Wir setzen isAdmin auf true, damit die Optionen zum Hinzufügen und Entfernen
// von Teammitgliedern immer angezeigt werden
setIsAdmin(true);
setUserRole('admin');
// Prüfe, ob der Benutzer ein Teammitglied ist
const { data: memberData, error: memberError } = await supabase
.from('team_members')
.select('user_id, team_id')
.eq('user_id', userId)
.eq('team_id', teamId);
if (memberError) {
console.error('Fehler beim Prüfen der Mitgliedschaft:', memberError);
}
// Wenn der Benutzer kein Mitglied ist, setzen wir die Rolle auf null
if (!memberData || memberData.length === 0) {
setUserRole(null);
}
} catch (error) {
console.error('Fehler beim Prüfen der Benutzerrolle:', error);
}
}
async function fetchTeamDetails(teamId: string) {
try {
const { data, error } = await supabase
.from('teams')
.select('id, name, organization_id, created_at')
.eq('id', teamId)
.single();
if (error) throw error;
if (data) {
// Hole den Organisationsnamen
const { data: orgData, error: orgError } = await supabase
.from('organizations')
.select('name')
.eq('id', data.organization_id)
.single();
if (orgError) throw orgError;
setTeamDetails({
...data,
organization_name: orgData?.name || 'Unbekannte Organisation'
});
// Setze den neuen Teamnamen
setNewTeamName(data.name);
}
} catch (error) {
console.error('Fehler beim Abrufen der Team-Details:', error);
Alert.alert('Fehler', 'Es ist ein Fehler beim Laden der Team-Details aufgetreten.');
}
}
const startEditing = () => {
setIsEditing(true);
if (teamDetails) {
setNewTeamName(teamDetails.name);
}
};
const cancelEditing = () => {
setIsEditing(false);
if (teamDetails) {
setNewTeamName(teamDetails.name);
}
};
const updateTeamName = async () => {
if (!newTeamName.trim()) {
Alert.alert('Fehler', 'Der Teamname darf nicht leer sein.');
return;
}
if (teamDetails && newTeamName.trim() === teamDetails.name) {
setIsEditing(false);
return;
}
setUpdatingName(true);
try {
const { error } = await supabase
.from('teams')
.update({ name: newTeamName.trim() })
.eq('id', teamId);
if (error) throw error;
// Aktualisiere die Teamdetails
if (teamDetails) {
setTeamDetails({
...teamDetails,
name: newTeamName.trim()
});
}
setIsEditing(false);
Alert.alert('Erfolg', 'Der Teamname wurde erfolgreich aktualisiert.');
} catch (error) {
console.error('Fehler beim Aktualisieren des Teamnamens:', error);
Alert.alert('Fehler', 'Es ist ein Fehler beim Aktualisieren des Teamnamens aufgetreten.');
} finally {
setUpdatingName(false);
}
};
async function fetchTeamMembers(teamId: string) {
try {
setLoading(true);
// Hole alle Teammitglieder basierend auf der tatsächlichen Datenbankstruktur
const { data: memberData, error: memberError } = await supabase
.from('team_members')
.select('user_id, team_id, allocated_credits, used_credits, created_at, updated_at')
.eq('team_id', teamId);
if (memberError) throw memberError;
if (memberData && memberData.length > 0) {
// Hole die Benutzerprofile für jedes Mitglied
const userIds = memberData.map(member => member.user_id);
// Abfrage der Profilinformationen aus der profiles-Tabelle
const { data: userData, error: userError } = await supabase
.from('profiles')
.select('id, first_name, last_name')
.in('id', userIds);
if (userError) throw userError;
// Füge Benutzerinformationen zu den Mitgliedern hinzu
const enhancedMembers = memberData.map(member => {
const user = userData?.find(u => u.id === member.user_id);
return {
...member,
first_name: user?.first_name,
last_name: user?.last_name
};
});
setMembers(enhancedMembers as TeamMember[]);
} else {
setMembers([]);
}
} catch (error) {
console.error('Fehler beim Abrufen der Teammitglieder:', error);
Alert.alert('Fehler', 'Es ist ein Fehler beim Laden der Teammitglieder aufgetreten.');
} finally {
setLoading(false);
}
}
async function inviteMember() {
if (!newMemberEmail || !newMemberEmail.includes('@')) {
Alert.alert('Fehler', 'Bitte geben Sie eine gültige E-Mail-Adresse ein.');
return;
}
if (!teamId || !isAdmin) {
Alert.alert('Fehler', 'Sie haben keine Berechtigung, Mitglieder einzuladen.');
return;
}
try {
setInviting(true);
// Suche nach dem Benutzer mit dieser E-Mail
const { data: profiles, error: profilesError } = await supabase
.from('profiles')
.select('id, email')
.ilike('email', newMemberEmail.trim().toLowerCase());
if (profilesError) {
console.error('Fehler beim Suchen des Benutzers:', profilesError);
Alert.alert('Fehler', 'Benutzer konnte nicht gefunden werden.');
return;
}
if (!profiles || profiles.length === 0) {
// Wenn der Benutzer nicht gefunden wurde, zeigen wir eine Meldung an
Alert.alert(
'Benutzer nicht gefunden',
'Möchten Sie eine Einladung an diese E-Mail-Adresse senden?',
[
{
text: 'Abbrechen',
style: 'cancel'
},
{
text: 'Einladen',
onPress: () => {
// Hier könnte eine Einladungs-E-Mail gesendet werden
// Für jetzt zeigen wir nur eine Erfolgsmeldung an
Alert.alert('Erfolg', `Eine Einladung wurde an ${newMemberEmail} gesendet.`);
setNewMemberEmail('');
}
}
]
);
return;
}
const userId = profiles[0].id;
// Prüfe, ob der Benutzer bereits Mitglied ist
const { data: existingMember, error: existingError } = await supabase
.from('team_members')
.select('user_id, team_id')
.eq('user_id', userId)
.eq('team_id', teamId);
if (existingError) throw existingError;
if (existingMember && existingMember.length > 0) {
Alert.alert('Information', 'Dieser Benutzer ist bereits Mitglied des Teams.');
return;
}
// Füge den Benutzer zum Team hinzu
const { error: addError } = await supabase
.from('team_members')
.insert([
{
user_id: userId,
team_id: teamId,
allocated_credits: 0,
used_credits: 0
}
]);
if (addError) throw addError;
Alert.alert('Erfolg', 'Benutzer wurde erfolgreich zum Team hinzugefügt.');
setNewMemberEmail('');
fetchTeamMembers(teamId);
} catch (error) {
console.error('Fehler beim Einladen des Benutzers:', error);
Alert.alert('Fehler', 'Es ist ein Fehler beim Einladen des Benutzers aufgetreten.');
} finally {
setInviting(false);
}
}
// Direktes Entfernen ohne Alert-Dialog
async function removeMember(userId: string, memberName: string) {
if (!isAdmin) {
Alert.alert('Fehler', 'Sie haben keine Berechtigung, Mitglieder zu entfernen.');
return;
}
console.log(`Versuche, Mitglied zu entfernen: ${memberName} (${userId}) aus Team ${teamId}`);
try {
console.log('Sende Löschanfrage an Supabase...');
console.log(`Parameter: user_id=${userId}, team_id=${teamId}`);
// Beachte, dass team_members einen zusammengesetzten Primärschlüssel aus team_id und user_id hat
const { data, error } = await supabase
.from('team_members')
.delete()
.eq('user_id', userId)
.eq('team_id', teamId)
.select();
if (error) {
console.error('Supabase-Fehler beim Entfernen:', error);
Alert.alert('Fehler', `Fehler beim Entfernen des Mitglieds: ${error.message}`);
return;
}
console.log('Löschantwort von Supabase:', data);
Alert.alert('Erfolg', 'Mitglied wurde erfolgreich aus dem Team entfernt.');
// Aktualisiere die Mitgliederliste
fetchTeamMembers(teamId);
} catch (error) {
console.error('Fehler beim Entfernen des Mitglieds:', error);
Alert.alert('Fehler', 'Es ist ein Fehler beim Entfernen des Mitglieds aufgetreten. Bitte überprüfen Sie die Konsole für weitere Details.');
}
}
// Funktion zum Starten der Bearbeitung des Credit-Limits für ein Mitglied
const startEditingLimit = (userId: string, currentLimit: number) => {
if (!isAdmin) {
Alert.alert('Fehler', 'Sie haben keine Berechtigung, Mitgliederlimits zu bearbeiten.');
return;
}
setEditingMemberId(userId);
setNewCreditLimit(currentLimit.toString());
};
// Funktion zum Abbrechen der Bearbeitung
const cancelEditingLimit = () => {
setEditingMemberId(null);
setNewCreditLimit('');
};
// Funktion zum Aktualisieren des Credit-Limits eines Mitglieds
const updateMemberLimit = async () => {
if (!editingMemberId || !teamId) {
Alert.alert('Fehler', 'Mitglied oder Team-ID nicht gefunden.');
return;
}
const creditLimit = parseInt(newCreditLimit);
if (isNaN(creditLimit) || creditLimit < 0) {
Alert.alert('Fehler', 'Bitte geben Sie einen gültigen Wert für das Credit-Limit ein.');
return;
}
setUpdatingLimit(true);
try {
// Aktualisiere das Credit-Limit in der Datenbank
const { error } = await supabase
.from('team_members')
.update({ allocated_credits: creditLimit })
.eq('user_id', editingMemberId)
.eq('team_id', teamId);
if (error) throw error;
// Aktualisiere die lokale Mitgliederliste
setMembers(members.map(member => {
if (member.user_id === editingMemberId) {
return { ...member, allocated_credits: creditLimit };
}
return member;
}));
Alert.alert('Erfolg', 'Das Credit-Limit wurde erfolgreich aktualisiert.');
setEditingMemberId(null);
} catch (error) {
console.error('Fehler beim Aktualisieren des Credit-Limits:', error);
Alert.alert('Fehler', 'Es ist ein Fehler beim Aktualisieren des Credit-Limits aufgetreten.');
} finally {
setUpdatingLimit(false);
}
};
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleDateString('de-DE', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
};
if (!session) {
return (
<View className={`flex-1 ${isDarkMode ? 'bg-gray-800' : 'bg-white'} rounded-lg p-4 m-2.5 shadow`}>
<Text className={`text-base ${isDarkMode ? 'text-gray-300' : 'text-gray-600'} text-center p-5`}>
Bitte melden Sie sich an, um Teammitglieder zu sehen.
</Text>
</View>
);
}
if (loading) {
return (
<View className={`flex-1 ${isDarkMode ? 'bg-gray-800' : 'bg-white'} rounded-lg p-4 m-2.5 shadow`}>
<ActivityIndicator size="large" color="#0055FF" />
<Text className={`text-base ${isDarkMode ? 'text-gray-300' : 'text-gray-600'} text-center mt-2.5`}>Lade Teammitglieder...</Text>
</View>
);
}
return (
<View className="flex-1">
{teamDetails && (
<View className={`${isDarkMode ? 'bg-gray-800' : 'bg-white'} rounded-lg p-4 m-2.5 shadow`}>
{isEditing ? (
<View>
<View className="flex-row items-center mb-3">
<Text className={`text-lg font-bold ${isDarkMode ? 'text-gray-100' : 'text-gray-800'} mr-2`}>Teamname bearbeiten:</Text>
</View>
<View className="flex-row items-center">
<TextInput
className={`flex-1 ${isDarkMode ? 'bg-gray-700 border-gray-600 text-white' : 'bg-gray-50 border-gray-300 text-gray-800'} border rounded-lg px-3 py-2 mr-2`}
value={newTeamName}
onChangeText={setNewTeamName}
placeholder="Teamname"
placeholderTextColor={isDarkMode ? '#9CA3AF' : '#6B7280'}
/>
</View>
<View className="flex-row justify-end mt-3 mb-2">
<TouchableOpacity
className={`flex-row items-center py-2 px-4 rounded-lg mr-2 ${isDarkMode ? 'bg-gray-700' : 'bg-gray-200'}`}
onPress={cancelEditing}
disabled={updatingName}
>
<Text className={`${isDarkMode ? 'text-gray-300' : 'text-gray-700'} font-semibold`}>Abbrechen</Text>
</TouchableOpacity>
<TouchableOpacity
className={`flex-row items-center py-2 px-4 rounded-lg ${isDarkMode ? 'bg-blue-700' : 'bg-blue-600'}`}
onPress={updateTeamName}
disabled={updatingName}
>
{updatingName ? (
<Text className="text-white font-semibold">Speichern...</Text>
) : (
<Text className="text-white font-semibold">Speichern</Text>
)}
</TouchableOpacity>
</View>
<Text className={`text-sm ${isDarkMode ? 'text-gray-300' : 'text-gray-600'} mb-3`}>
Organisation: {teamDetails.organization_name}
</Text>
<Text className={`text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`}>
Erstellt am: {formatDate(teamDetails.created_at)}
</Text>
</View>
) : (
<View>
<View className="flex-row items-center justify-between mb-1">
<Text className={`text-xl font-bold ${isDarkMode ? 'text-gray-100' : 'text-gray-800'}`}>{teamDetails.name}</Text>
{isAdmin && (
<TouchableOpacity
className={`p-2 rounded-full ${isDarkMode ? 'bg-gray-700' : 'bg-gray-100'}`}
onPress={startEditing}
>
<FontAwesome5 name="edit" size={16} color={isDarkMode ? '#93C5FD' : '#0055FF'} />
</TouchableOpacity>
)}
</View>
<Text className={`text-sm ${isDarkMode ? 'text-gray-300' : 'text-gray-600'} mb-3`}>
Organisation: {teamDetails.organization_name}
</Text>
<Text className={`text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`}>
Erstellt am: {formatDate(teamDetails.created_at)}
</Text>
</View>
)}
</View>
)}
<View className={`${isDarkMode ? 'bg-gray-800' : 'bg-white'} rounded-lg p-4 m-2.5 shadow`}>
<Text className={`text-lg font-semibold ${isDarkMode ? 'text-gray-100' : 'text-gray-800'} mb-4`}>Teammitglieder</Text>
{isAdmin && (
<View className={`mb-4 ${isDarkMode ? 'bg-gray-700' : 'bg-gray-50'} p-3 rounded-lg`}>
<Text className={`text-sm font-medium ${isDarkMode ? 'text-gray-200' : 'text-gray-700'} mb-2`}>Neues Mitglied einladen</Text>
<View className="flex-row">
<TextInput
className={`flex-1 ${isDarkMode ? 'bg-gray-600 border-gray-500 text-white' : 'bg-white border-gray-300 text-gray-800'} border rounded-lg px-3 py-2 mr-2`}
placeholder="E-Mail-Adresse"
placeholderTextColor={isDarkMode ? '#9CA3AF' : '#6B7280'}
value={newMemberEmail}
onChangeText={setNewMemberEmail}
autoCapitalize="none"
keyboardType="email-address"
/>
<TouchableOpacity
className="bg-blue-600 rounded-lg px-4 py-2 items-center justify-center"
onPress={inviteMember}
disabled={inviting}
>
{inviting ? (
<ActivityIndicator size="small" color="white" />
) : (
<Text className="text-white font-medium">Einladen</Text>
)}
</TouchableOpacity>
</View>
</View>
)}
{members.length === 0 ? (
<View className="items-center justify-center py-8">
<FontAwesome5 name="users-slash" size={50} color={isDarkMode ? '#4B5563' : '#ccc'} />
<Text className={`text-base ${isDarkMode ? 'text-gray-300' : 'text-gray-600'} text-center mt-4`}>
Keine Mitglieder in diesem Team gefunden.
</Text>
</View>
) : (
<FlatList
data={members}
keyExtractor={(item) => item.user_id}
renderItem={({ item }) => (
<View className={`flex-row items-center justify-between ${isDarkMode ? 'bg-gray-700' : 'bg-gray-50'} p-3 rounded-lg mb-2`}>
<View className="flex-1">
<Text className={`text-base font-medium ${isDarkMode ? 'text-gray-100' : 'text-gray-800'}`}>
{item.first_name && item.last_name
? `${item.first_name} ${item.last_name}`
: `Benutzer ${item.user_id.substring(0, 8)}...`}
</Text>
<View className="flex-row items-center mt-1">
{editingMemberId === item.user_id ? (
<View className="flex-row items-center mr-4">
<Text className={`text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-500'} mr-2`}>
Credits:
</Text>
<TextInput
className={`${isDarkMode ? 'bg-gray-600 border-gray-500 text-white' : 'bg-white border-gray-300 text-gray-800'} border rounded px-2 py-1 w-16 text-xs`}
value={newCreditLimit}
onChangeText={setNewCreditLimit}
keyboardType="numeric"
autoFocus
/>
<TouchableOpacity
onPress={updateMemberLimit}
className="ml-2 p-1"
disabled={updatingLimit}
>
<FontAwesome5 name="check" size={12} color={isDarkMode ? '#10B981' : '#059669'} />
</TouchableOpacity>
<TouchableOpacity
onPress={cancelEditingLimit}
className="ml-1 p-1"
disabled={updatingLimit}
>
<FontAwesome5 name="times" size={12} color={isDarkMode ? '#F87171' : '#EF4444'} />
</TouchableOpacity>
</View>
) : (
<TouchableOpacity
onPress={() => isAdmin && startEditingLimit(item.user_id, item.allocated_credits)}
className={`flex-row items-center mr-4 ${isAdmin ? 'opacity-100' : 'opacity-80'}`}
>
<Text className={`text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`}>
Zugewiesene Credits: {item.allocated_credits}
</Text>
{isAdmin && (
<FontAwesome5
name="edit"
size={10}
color={isDarkMode ? '#93C5FD' : '#0055FF'}
style={{ marginLeft: 4 }}
/>
)}
</TouchableOpacity>
)}
<Text className={`text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`}>
Genutzte Credits: {item.used_credits}
</Text>
</View>
</View>
{isAdmin && (
<View className="flex-row">
<TouchableOpacity
className={`${isDarkMode ? 'bg-blue-900' : 'bg-blue-100'} p-2 rounded-full mr-2`}
onPress={() => startEditingLimit(item.user_id, item.allocated_credits)}
>
<FontAwesome5 name="sliders-h" size={16} color={isDarkMode ? '#93C5FD' : '#0055FF'} />
</TouchableOpacity>
<TouchableOpacity
className={`${isDarkMode ? 'bg-red-900' : 'bg-red-100'} p-2 rounded-full`}
onPress={() => removeMember(item.user_id, item.first_name && item.last_name
? `${item.first_name} ${item.last_name}`
: `Benutzer ${item.user_id.substring(0, 8)}...`)}
>
<FontAwesome5 name="user-minus" size={16} color={isDarkMode ? '#F87171' : '#EF4444'} />
</TouchableOpacity>
</View>
)}
</View>
)}
/>
)}
</View>
</View>
);
}