refactor(manacore-mobile): migrate from Supabase to mana-core-auth

Complete migration of ManaCore mobile app from direct Supabase access
to mana-core-auth API. Removes @supabase/supabase-js dependency entirely.

New files:
- context/AuthProvider.tsx: mana-core-auth integration via @manacore/shared-auth
- services/api.ts: ManaCoreApi client wrapping mana-core-auth REST endpoints

Migrated (17 files):
- Auth: signIn/signUp/signOut → @manacore/shared-auth with SecureStore
- Profiles: supabase.from('profiles') → /api/v1/auth/profile
- Organizations: supabase.from('organizations') → /api/v1/auth/organizations
- Credits: supabase.from('credit_transactions') → /api/v1/credits/balance
- Members: supabase.from('user_roles') → /api/v1/auth/organizations/:id/members

Simplified (teams not yet in mana-core-auth):
- CreateTeam, TeamList, TeamMembers → placeholder with TODO
- SendMana → shows balance only, transfer coming later

Deleted:
- utils/supabase.ts, utils/memoryStorage.ts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-03-23 11:41:48 +01:00
parent 99d16673bd
commit 343f30e321
20 changed files with 914 additions and 3487 deletions

View file

@ -1,110 +1,75 @@
import React, { useState, useEffect } from 'react';
import React, { useState } from 'react';
import { Stack } from 'expo-router';
import {
ScrollView,
Text,
View,
Image,
TouchableOpacity,
ActivityIndicator,
Alert,
Platform,
Linking,
} from 'react-native';
import { ScrollView, Text, View, TouchableOpacity, Platform, Linking, Alert } from 'react-native';
import { FontAwesome5 } from '@expo/vector-icons';
import { Container } from '~/components/Container';
import { useTheme } from '../../utils/themeContext';
import { supabase } from '../../utils/supabase';
// Interface für die Satellite-Daten
interface Satellite {
// TODO: App-Liste (Satellites) war zuvor in Supabase gespeichert.
// Bis diese Daten über mana-core-auth oder einen eigenen Endpunkt verfügbar sind,
// verwenden wir eine statische Liste der bekannten ManaCore-Apps.
interface AppInfo {
id: string;
name: string;
description: string;
brand_logo: string;
created_at: string;
icon: string;
link_web?: string;
link_ios?: string;
link_android?: string;
}
const MANACORE_APPS: AppInfo[] = [
{
id: 'chat',
name: 'Chat',
description: 'KI-Chat-Anwendung mit verschiedenen Modellen',
icon: 'comments',
link_web: 'https://chat.mana.how',
},
{
id: 'picture',
name: 'Picture',
description: 'KI-Bildgenerierung',
icon: 'image',
link_web: 'https://picture.mana.how',
},
{
id: 'zitare',
name: 'Zitare',
description: 'Tägliche Inspirationszitate',
icon: 'quote-left',
link_web: 'https://zitare.mana.how',
},
{
id: 'manadeck',
name: 'ManaDeck',
description: 'Karten- und Deck-Verwaltung',
icon: 'layer-group',
link_web: 'https://manadeck.mana.how',
},
{
id: 'contacts',
name: 'Contacts',
description: 'Kontaktverwaltung',
icon: 'address-book',
},
];
export default function Apps() {
const { isDarkMode } = useTheme();
const [satellites, setSatellites] = useState<Satellite[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchSatellites();
}, []);
const fetchSatellites = async () => {
const openAppLink = async (app: AppInfo) => {
try {
setLoading(true);
// Hole die Satellites-Daten aus der Supabase-Datenbank
const { data, error } = await supabase.from('satellites').select('*').order('name');
if (error) {
throw error;
}
if (data) {
console.log('Satellites geladen:', data.length);
setSatellites(data);
}
} catch (error) {
console.error('Fehler beim Laden der Satellites:', error);
Alert.alert(
'Fehler',
'Beim Laden der Apps ist ein Fehler aufgetreten. Bitte versuche es später erneut.'
);
} finally {
setLoading(false);
}
};
const handleAppClick = (satellite: Satellite) => {
// Öffne die App-Details in einem Alert
console.log('App angeklickt:', satellite.name);
Alert.alert(satellite.name, satellite.description, [{ text: 'OK' }]);
};
const openAppLink = async (satellite: Satellite) => {
try {
let linkToOpen: string | undefined;
// Wähle den entsprechenden Link basierend auf der Plattform
if (Platform.OS === 'ios') {
linkToOpen = satellite.link_ios;
} else if (Platform.OS === 'android') {
linkToOpen = satellite.link_android;
} else {
// Für Web oder wenn die plattformspezifischen Links nicht verfügbar sind
linkToOpen = satellite.link_web;
}
// Fallback: Wenn kein plattformspezifischer Link vorhanden ist, versuche den Web-Link
if (!linkToOpen) {
linkToOpen = satellite.link_web;
}
// Wenn kein Link verfügbar ist, zeige eine Meldung an
if (!linkToOpen) {
Alert.alert('Bald verfügbar', `${satellite.name} wird bald verfügbar sein.`, [
{ text: 'OK' },
]);
if (!app.link_web) {
Alert.alert('Bald verfügbar', `${app.name} wird bald verfügbar sein.`, [{ text: 'OK' }]);
return;
}
// Prüfe, ob der Link geöffnet werden kann
const canOpen = await Linking.canOpenURL(linkToOpen);
const canOpen = await Linking.canOpenURL(app.link_web);
if (canOpen) {
// Öffne den Link
await Linking.openURL(linkToOpen);
await Linking.openURL(app.link_web);
} else {
Alert.alert('Fehler', `Der Link für ${satellite.name} kann nicht geöffnet werden.`, [
Alert.alert('Fehler', `Der Link für ${app.name} kann nicht geöffnet werden.`, [
{ text: 'OK' },
]);
}
@ -134,95 +99,50 @@ export default function Apps() {
Entdecke Apps, die mit Manacore verbunden werden können
</Text>
{loading ? (
<View
className={`flex-1 items-center justify-center p-10 ${isDarkMode ? 'bg-gray-800' : 'bg-white'} rounded-lg shadow`}
>
<ActivityIndicator size="large" color={isDarkMode ? '#93C5FD' : '#0055FF'} />
<Text
className={`mt-4 text-base ${isDarkMode ? 'text-gray-300' : 'text-gray-600'}`}
<View className="flex-row flex-wrap justify-between">
{MANACORE_APPS.map((app) => (
<TouchableOpacity
key={app.id}
className={`mb-4 w-[48%] overflow-hidden rounded-xl shadow ${isDarkMode ? 'bg-gray-800' : 'bg-white'}`}
onPress={() => openAppLink(app)}
activeOpacity={0.7}
>
Lade Apps...
</Text>
</View>
) : satellites.length === 0 ? (
<View
className={`p-8 ${isDarkMode ? 'bg-gray-800' : 'bg-white'} items-center rounded-lg shadow`}
>
<FontAwesome5 name="rocket" size={50} color={isDarkMode ? '#4B5563' : '#9CA3AF'} />
<Text
className={`mt-4 text-center text-lg font-medium ${isDarkMode ? 'text-gray-300' : 'text-gray-600'}`}
>
Keine Apps verfügbar
</Text>
<Text
className={`mt-2 text-center text-sm ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`}
>
Derzeit sind keine Apps verfügbar. Schaue später noch einmal vorbei.
</Text>
</View>
) : (
<View className="flex-row flex-wrap justify-between">
{satellites.map((satellite) => (
<TouchableOpacity
key={satellite.id}
className={`mb-4 w-[48%] overflow-hidden rounded-xl shadow ${isDarkMode ? 'bg-gray-800' : 'bg-white'}`}
onPress={() => handleAppClick(satellite)}
activeOpacity={0.7}
>
<View className="items-center p-4">
{satellite.brand_logo ? (
<Image
source={{ uri: satellite.brand_logo }}
className="mb-3 h-16 w-16 rounded-lg"
resizeMode="contain"
// Fallback für den Fall, dass das Bild nicht geladen werden kann
onError={() =>
console.log('Fehler beim Laden des Logos:', satellite.brand_logo)
}
/>
) : (
<View className="mb-3 h-16 w-16 items-center justify-center rounded-lg bg-gray-200">
<FontAwesome5
name="rocket"
size={24}
color={isDarkMode ? '#4B5563' : '#9CA3AF'}
/>
</View>
)}
<Text
className={`mb-1 text-center text-base font-bold ${isDarkMode ? 'text-gray-100' : 'text-gray-800'}`}
>
{satellite.name}
</Text>
<Text
className={`text-center text-xs ${isDarkMode ? 'text-gray-300' : 'text-gray-600'}`}
numberOfLines={2}
ellipsizeMode="tail"
>
{satellite.description}
</Text>
<View className="items-center p-4">
<View className="mb-3 h-16 w-16 items-center justify-center rounded-lg bg-gray-200">
<FontAwesome5
name={app.icon}
size={24}
color={isDarkMode ? '#60A5FA' : '#0055FF'}
/>
</View>
<TouchableOpacity
className={`w-full py-2 ${isDarkMode ? 'bg-blue-700' : 'bg-blue-600'}`}
onPress={() => openAppLink(satellite)}
disabled={
!satellite.link_web && !satellite.link_ios && !satellite.link_android
}
<Text
className={`mb-1 text-center text-base font-bold ${isDarkMode ? 'text-gray-100' : 'text-gray-800'}`}
>
<Text className="text-center text-sm font-medium text-white">
{satellite.link_web || satellite.link_ios || satellite.link_android
? 'Öffnen'
: 'Bald verfügbar'}
</Text>
</TouchableOpacity>
{app.name}
</Text>
<Text
className={`text-center text-xs ${isDarkMode ? 'text-gray-300' : 'text-gray-600'}`}
numberOfLines={2}
ellipsizeMode="tail"
>
{app.description}
</Text>
</View>
<TouchableOpacity
className={`w-full py-2 ${isDarkMode ? 'bg-blue-700' : 'bg-blue-600'}`}
onPress={() => openAppLink(app)}
disabled={!app.link_web}
>
<Text className="text-center text-sm font-medium text-white">
{app.link_web ? 'Öffnen' : 'Bald verfügbar'}
</Text>
</TouchableOpacity>
))}
</View>
)}
</TouchableOpacity>
))}
</View>
<View className={`mt-4 rounded-lg p-4 ${isDarkMode ? 'bg-gray-800' : 'bg-gray-100'}`}>
<Text

View file

@ -9,43 +9,40 @@ import {
Alert,
ActivityIndicator,
Modal,
FlatList,
} from 'react-native';
import { FontAwesome5 } from '@expo/vector-icons';
import { Container } from '~/components/Container';
import { useTheme } from '../../../utils/themeContext';
import { supabase } from '../../../utils/supabase';
import { useAuth } from '../../../context/AuthProvider';
import { api } from '../../../services/api';
interface OrganizationDetails {
interface OrgMember {
id: string;
name: string;
total_credits: number;
used_credits: number;
created_at: string;
team_count: number;
user_count: number;
userId: string;
email?: string;
name?: string;
role: string;
createdAt: string;
}
export default function OrganizationDetails() {
const router = useRouter();
const { id: orgId, name: initialOrgName } = useLocalSearchParams<{ id: string; name: string }>();
const { isDarkMode } = useTheme();
const { user } = useAuth();
const [orgName, setOrgName] = useState(initialOrgName || '');
const [isEditing, setIsEditing] = useState(false);
const [newOrgName, setNewOrgName] = useState('');
const [loading, setLoading] = useState(false);
const [orgDetails, setOrgDetails] = useState<any>(null);
const [members, setMembers] = useState<OrgMember[]>([]);
const [loadingDetails, setLoadingDetails] = useState(true);
const [deletingOrg, setDeletingOrg] = useState(false);
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [orgDetails, setOrgDetails] = useState<OrganizationDetails | null>(null);
const [userRole, setUserRole] = useState<string>('');
const [loadingDetails, setLoadingDetails] = useState(true);
useEffect(() => {
if (initialOrgName) {
setOrgName(initialOrgName);
setNewOrgName(initialOrgName);
}
fetchOrganizationDetails();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [initialOrgName, orgId]);
@ -56,84 +53,20 @@ export default function OrganizationDetails() {
try {
setLoadingDetails(true);
// Hole die Organisation
const { data: org, error: orgError } = await supabase
.from('organizations')
.select('id, name, total_credits, used_credits, created_at')
.eq('id', orgId)
.single();
const { data: org, error: orgError } = await api.getOrganization(orgId);
if (orgError) throw new Error(orgError);
if (orgError) throw orgError;
// Hole die Anzahl der Teams in dieser Organisation
const { count: teamCount, error: teamCountError } = await supabase
.from('teams')
.select('id', { count: 'exact', head: true })
.eq('organization_id', orgId);
if (teamCountError) throw teamCountError;
// Hole die Anzahl der Benutzer in dieser Organisation
const { data: userRoles, error: userRolesError } = await supabase
.from('user_roles')
.select('user_id')
.eq('organization_id', orgId);
if (userRolesError) throw userRolesError;
// Entferne Duplikate (ein Benutzer kann mehrere Rollen haben)
const uniqueUserIds = [...new Set(userRoles.map((role) => role.user_id))];
// Hole die aktuelle Benutzerrolle
const {
data: { session },
} = await supabase.auth.getSession();
if (session?.user) {
const { data: currentUserRoles, error: currentUserRolesError } = await supabase
.from('user_roles')
.select('roles(name)')
.eq('user_id', session.user.id)
.eq('organization_id', orgId);
if (currentUserRolesError) throw currentUserRolesError;
// Finde die höchste Rolle
const roleHierarchy = {
system_admin: 4,
org_admin: 3,
team_admin: 2,
member: 1,
};
let highestRole = 'member';
let highestRoleValue = 0;
// Typensichere Iteration über die Benutzerrollen
// Wir müssen die Daten zuerst in das erwartete Format konvertieren
currentUserRoles.forEach((userRole: any) => {
// Prüfe, ob die Rolle existiert und ein Name vorhanden ist
const roleName = userRole.roles?.name;
if (
roleName &&
roleHierarchy[roleName as keyof typeof roleHierarchy] > highestRoleValue
) {
highestRole = roleName;
highestRoleValue = roleHierarchy[roleName as keyof typeof roleHierarchy];
}
});
setUserRole(highestRole);
if (org) {
setOrgDetails(org);
setOrgName(org.name);
}
setOrgDetails({
...org,
team_count: teamCount || 0,
user_count: uniqueUserIds.length,
});
setOrgName(org.name);
setNewOrgName(org.name);
} catch (error) {
// Fetch members
const { data: membersData } = await api.getOrgMembers(orgId);
if (membersData) {
setMembers(membersData);
}
} catch (error: any) {
console.error('Fehler beim Laden der Organisationsdetails:', error);
Alert.alert('Fehler', 'Es ist ein Fehler beim Laden der Organisationsdetails aufgetreten.');
} finally {
@ -145,60 +78,16 @@ export default function OrganizationDetails() {
router.replace('/organizations');
};
const toggleEditMode = () => {
if (isEditing) {
setIsEditing(false);
} else {
setNewOrgName(orgName);
setIsEditing(true);
}
};
const updateOrganizationName = async () => {
if (!orgId || !newOrgName.trim() || newOrgName.trim() === orgName) {
setIsEditing(false);
return;
}
try {
setLoading(true);
const { error } = await supabase
.from('organizations')
.update({ name: newOrgName.trim() })
.eq('id', orgId);
if (error) throw error;
setOrgName(newOrgName.trim());
setIsEditing(false);
Alert.alert('Erfolg', 'Der Organisationsname wurde erfolgreich aktualisiert.');
} catch (error) {
console.error('Fehler beim Aktualisieren des Organisationsnamens:', error);
Alert.alert(
'Fehler',
'Es ist ein Fehler beim Aktualisieren des Organisationsnamens aufgetreten.'
);
} finally {
setLoading(false);
}
};
const deleteOrg = () => {
console.log('Delete organization button clicked, orgId:', orgId);
// Modal öffnen statt Alert anzeigen
setShowDeleteModal(true);
};
const confirmDelete = () => {
console.log('Löschen bestätigt für Organisation:', orgId);
setShowDeleteModal(false);
handleOrgDeletion();
};
const cancelDelete = () => {
console.log('Löschen abgebrochen');
setShowDeleteModal(false);
};
@ -208,65 +97,24 @@ export default function OrganizationDetails() {
try {
setDeletingOrg(true);
console.log('Starte Löschvorgang für Organisation:', orgId);
const { error } = await api.deleteOrganization(orgId);
// 1. Prüfe, ob es Teams in dieser Organisation gibt
const { count: teamCount, error: teamCountError } = await supabase
.from('teams')
.select('id', { count: 'exact', head: true })
.eq('organization_id', orgId);
if (error) throw new Error(error);
if (teamCountError) throw teamCountError;
if (teamCount && teamCount > 0) {
Alert.alert(
'Fehler',
'Diese Organisation enthält noch Teams. Bitte lösche zuerst alle Teams, bevor du die Organisation löschst.'
);
return;
}
// 2. Lösche alle Benutzerrollen für diese Organisation
console.log('Lösche Benutzerrollen für Organisation:', orgId);
const { error: userRolesError } = await supabase
.from('user_roles')
.delete()
.eq('organization_id', orgId);
if (userRolesError) throw userRolesError;
// 3. Lösche die Organisation
console.log('Lösche Organisation:', orgId);
const { error: orgError } = await supabase.from('organizations').delete().eq('id', orgId);
if (orgError) throw orgError;
console.log('Organisation erfolgreich gelöscht');
// Erfolgsmeldung anzeigen und zurück zur Organisationsliste navigieren
Alert.alert('Erfolg', 'Die Organisation wurde erfolgreich gelöscht.', [
{
text: 'OK',
onPress: () => {
console.log('Navigating back to organizations list');
// Zurück zur Organisationsliste navigieren
router.replace('/organizations');
},
onPress: () => router.replace('/organizations'),
},
]);
// Auch ohne Klick auf OK zurück zur Organisationsliste navigieren (nach kurzer Verzögerung)
setTimeout(() => {
router.replace('/organizations');
}, 1000);
} catch (error: any) {
console.error('Fehler beim Löschen der Organisation:', error);
// Detaillierte Fehlermeldung anzeigen
Alert.alert(
'Fehler',
`Es ist ein Fehler beim Löschen der Organisation aufgetreten: ${error?.message || JSON.stringify(error)}`,
[{ text: 'OK' }]
`Es ist ein Fehler beim Löschen der Organisation aufgetreten: ${error?.message || 'Unbekannter Fehler'}`
);
} finally {
setDeletingOrg(false);
@ -284,14 +132,14 @@ export default function OrganizationDetails() {
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:
case 'owner':
return 'Eigentümer';
case 'admin':
return 'Administrator';
case 'member':
return 'Mitglied';
default:
return role;
}
};
@ -383,23 +231,21 @@ export default function OrganizationDetails() {
</Text>
</TouchableOpacity>
{(userRole === 'org_admin' || userRole === 'system_admin') && (
<TouchableOpacity
className={`flex-row items-center rounded-lg px-4 py-2 ${deletingOrg ? 'bg-gray-500' : 'bg-red-600'}`}
onPress={deleteOrg}
disabled={deletingOrg}
activeOpacity={0.7}
>
{deletingOrg ? (
<ActivityIndicator size="small" color="white" />
) : (
<>
<FontAwesome5 name="trash-alt" size={16} color="white" className="mr-2" />
<Text className="text-sm font-semibold text-white">Organisation löschen</Text>
</>
)}
</TouchableOpacity>
)}
<TouchableOpacity
className={`flex-row items-center rounded-lg px-4 py-2 ${deletingOrg ? 'bg-gray-500' : 'bg-red-600'}`}
onPress={deleteOrg}
disabled={deletingOrg}
activeOpacity={0.7}
>
{deletingOrg ? (
<ActivityIndicator size="small" color="white" />
) : (
<>
<FontAwesome5 name="trash-alt" size={16} color="white" className="mr-2" />
<Text className="text-sm font-semibold text-white">Organisation löschen</Text>
</>
)}
</TouchableOpacity>
</View>
{loadingDetails ? (
@ -418,70 +264,18 @@ export default function OrganizationDetails() {
<View
className={`${isDarkMode ? 'bg-gray-800' : 'bg-white'} m-2.5 rounded-lg p-5 shadow`}
>
<View className="mb-4 flex-row items-center justify-between">
{isEditing ? (
<View className="flex-1 flex-row items-center">
<TextInput
className={`h-12 flex-1 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={newOrgName}
onChangeText={setNewOrgName}
placeholder="Organisationsname"
placeholderTextColor={isDarkMode ? '#9CA3AF' : '#9CA3AF'}
autoFocus
/>
<TouchableOpacity
className="ml-2 p-2"
onPress={updateOrganizationName}
disabled={loading}
>
{loading ? (
<ActivityIndicator
size="small"
color={isDarkMode ? '#93C5FD' : '#0055FF'}
/>
) : (
<FontAwesome5
name="check"
size={20}
color={isDarkMode ? '#93C5FD' : '#0055FF'}
/>
)}
</TouchableOpacity>
<TouchableOpacity className="ml-2 p-2" onPress={toggleEditMode}>
<FontAwesome5
name="times"
size={20}
color={isDarkMode ? '#F87171' : '#EF4444'}
/>
</TouchableOpacity>
</View>
) : (
<>
<View className="flex-row items-center">
<FontAwesome5
name="building"
size={24}
color={isDarkMode ? '#93C5FD' : '#0055FF'}
className="mr-3"
/>
<Text
className={`text-2xl font-bold ${isDarkMode ? 'text-gray-100' : 'text-gray-800'}`}
>
{orgName}
</Text>
</View>
{(userRole === 'org_admin' || userRole === 'system_admin') && (
<TouchableOpacity className="p-2" onPress={toggleEditMode}>
<FontAwesome5
name="edit"
size={18}
color={isDarkMode ? '#93C5FD' : '#0055FF'}
/>
</TouchableOpacity>
)}
</>
)}
<View className="mb-4 flex-row items-center">
<FontAwesome5
name="building"
size={24}
color={isDarkMode ? '#93C5FD' : '#0055FF'}
className="mr-3"
/>
<Text
className={`text-2xl font-bold ${isDarkMode ? 'text-gray-100' : 'text-gray-800'}`}
>
{orgName}
</Text>
</View>
{orgDetails && (
@ -495,20 +289,7 @@ export default function OrganizationDetails() {
<Text
className={`font-medium ${isDarkMode ? 'text-gray-100' : 'text-gray-800'}`}
>
{formatDate(orgDetails.created_at)}
</Text>
</View>
<View
className={`flex-row justify-between border-t py-3 ${isDarkMode ? 'border-gray-700' : 'border-gray-200'}`}
>
<Text className={`${isDarkMode ? 'text-gray-300' : 'text-gray-600'}`}>
Teams
</Text>
<Text
className={`font-medium ${isDarkMode ? 'text-gray-100' : 'text-gray-800'}`}
>
{orgDetails.team_count}
{formatDate(orgDetails.createdAt)}
</Text>
</View>
@ -521,111 +302,75 @@ export default function OrganizationDetails() {
<Text
className={`font-medium ${isDarkMode ? 'text-gray-100' : 'text-gray-800'}`}
>
{orgDetails.user_count}
{members.length}
</Text>
</View>
<View
className={`flex-row justify-between border-t py-3 ${isDarkMode ? 'border-gray-700' : 'border-gray-200'}`}
>
<Text className={`${isDarkMode ? 'text-gray-300' : 'text-gray-600'}`}>
Deine Rolle
</Text>
{orgDetails.slug && (
<View
className={`rounded-full px-2 py-0.5 ${userRole === 'system_admin' ? 'bg-red-600' : userRole === 'org_admin' ? 'bg-orange-500' : userRole === 'team_admin' ? 'bg-green-600' : 'bg-gray-600'}`}
className={`flex-row justify-between border-t py-3 ${isDarkMode ? 'border-gray-700' : 'border-gray-200'}`}
>
<Text className="text-xs font-medium text-white">
{getRoleName(userRole)}
<Text className={`${isDarkMode ? 'text-gray-300' : 'text-gray-600'}`}>
Slug
</Text>
<Text
className={`font-medium ${isDarkMode ? 'text-gray-100' : 'text-gray-800'}`}
>
{orgDetails.slug}
</Text>
</View>
</View>
)}
</>
)}
</View>
{/* Mitglieder-Liste */}
<View
className={`${isDarkMode ? 'bg-gray-800' : 'bg-white'} m-2.5 rounded-lg p-5 shadow`}
>
<View className="mb-4 flex-row items-center">
<FontAwesome5
name="coins"
size={20}
color={isDarkMode ? '#93C5FD' : '#0055FF'}
className="mr-2"
/>
<Text
className={`text-xl font-bold ${isDarkMode ? 'text-gray-100' : 'text-gray-800'}`}
>
Kredit-Übersicht
<Text
className={`mb-4 text-xl font-bold ${isDarkMode ? 'text-gray-100' : 'text-gray-800'}`}
>
Mitglieder
</Text>
{members.length === 0 ? (
<Text className={`text-center ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`}>
Keine Mitglieder gefunden.
</Text>
</View>
{orgDetails && (
<View className="flex-row justify-between">
) : (
members.map((member) => (
<View
className="mr-2 flex-1 items-center rounded-lg bg-opacity-20 p-3"
style={{
backgroundColor: isDarkMode
? 'rgba(147, 197, 253, 0.1)'
: 'rgba(0, 85, 255, 0.1)',
}}
key={member.id}
className={`mb-2 flex-row items-center justify-between rounded-lg p-3 ${isDarkMode ? 'bg-gray-700' : 'bg-gray-50'}`}
>
<Text
className={`text-sm ${isDarkMode ? 'text-gray-300' : 'text-gray-600'} mb-1`}
<View>
<Text
className={`text-base font-medium ${isDarkMode ? 'text-gray-100' : 'text-gray-800'}`}
>
{member.name ||
member.email ||
`Benutzer ${member.userId.substring(0, 8)}...`}
</Text>
{member.email && member.name && (
<Text
className={`text-sm ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`}
>
{member.email}
</Text>
)}
</View>
<View
className={`rounded-full px-2 py-0.5 ${member.role === 'owner' ? 'bg-red-600' : member.role === 'admin' ? 'bg-orange-500' : 'bg-gray-600'}`}
>
Gesamt
</Text>
<Text
className={`text-2xl font-bold ${isDarkMode ? 'text-gray-100' : 'text-gray-800'}`}
>
{orgDetails.total_credits}
</Text>
<Text className="text-xs font-medium text-white">
{getRoleName(member.role)}
</Text>
</View>
</View>
<View
className="mx-1 flex-1 items-center rounded-lg bg-opacity-20 p-3"
style={{
backgroundColor: isDarkMode
? 'rgba(147, 197, 253, 0.1)'
: 'rgba(0, 85, 255, 0.1)',
}}
>
<Text
className={`text-sm ${isDarkMode ? 'text-gray-300' : 'text-gray-600'} mb-1`}
>
Verwendet
</Text>
<Text
className={`text-2xl font-bold ${isDarkMode ? 'text-gray-100' : 'text-gray-800'}`}
>
{orgDetails.used_credits}
</Text>
</View>
<View
className="ml-2 flex-1 items-center rounded-lg bg-opacity-20 p-3"
style={{
backgroundColor: isDarkMode
? 'rgba(147, 197, 253, 0.1)'
: 'rgba(0, 85, 255, 0.1)',
}}
>
<Text
className={`text-sm ${isDarkMode ? 'text-gray-300' : 'text-gray-600'} mb-1`}
>
Verfügbar
</Text>
<Text
className={`text-2xl font-bold ${orgDetails.total_credits - orgDetails.used_credits > 0 ? (isDarkMode ? 'text-green-400' : 'text-green-600') : isDarkMode ? 'text-red-400' : 'text-red-600'}`}
>
{orgDetails.total_credits - orgDetails.used_credits}
</Text>
</View>
</View>
))
)}
</View>
{/* Hier könnte später eine Liste der Teams oder Mitglieder angezeigt werden */}
</>
)}
</ScrollView>

View file

@ -13,26 +13,15 @@ import { FontAwesome5 } from '@expo/vector-icons';
import { Container } from '~/components/Container';
import { useTheme } from '~/utils/themeContext';
import type { ThemeMode } from '~/utils/themeContext';
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;
}
import { useAuth } from '~/context/AuthProvider';
import { api } from '~/services/api';
export default function SettingsScreen() {
const { themeMode, setThemeMode, isDarkMode } = useTheme();
const [session, setSession] = useState<Session | null>(null);
const { user, signOut } = useAuth();
const [loading, setLoading] = useState(false);
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [profile, setProfile] = useState<Profile | null>(null);
const [name, setName] = useState('');
const [creditBalance, setCreditBalance] = useState<number | null>(null);
// Funktion zum Ändern des Theme-Modus
const changeTheme = (mode: ThemeMode) => {
@ -40,42 +29,28 @@ export default function SettingsScreen() {
};
useEffect(() => {
// Prüfe den aktuellen Authentifizierungsstatus
supabase.auth.getSession().then(({ data: { session } }) => {
setSession(session);
if (session) getProfile(session);
});
if (user) loadProfile();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [user]);
// Abonniere Authentifizierungsänderungen
const {
data: { subscription },
} = supabase.auth.onAuthStateChange((_event, session) => {
setSession(session);
if (session) getProfile(session);
});
return () => subscription.unsubscribe();
}, []);
async function getProfile(currentSession: Session) {
async function loadProfile() {
try {
setLoading(true);
const { user } = currentSession;
const { data, error } = await supabase
.from('profiles')
.select('*')
.eq('id', user.id)
.single();
const { data, error } = await api.getProfile();
if (error) {
throw error;
throw new Error(error);
}
if (data) {
setProfile(data);
setFirstName(data.first_name || '');
setLastName(data.last_name || '');
setName(data.name || '');
}
// Also load credit balance
const { data: creditData } = await api.getCreditBalance();
if (creditData) {
setCreditBalance(creditData.balance);
}
} catch (error) {
if (error instanceof Error) {
@ -87,39 +62,15 @@ export default function SettingsScreen() {
}
async function updateProfile() {
if (!session) return;
if (!user) return;
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,
},
]);
const { error } = await api.updateProfile({ name });
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;
if (error) {
throw new Error(error);
}
Alert.alert('Erfolg', 'Profil erfolgreich aktualisiert!');
@ -132,11 +83,10 @@ export default function SettingsScreen() {
}
}
async function signOut() {
async function handleSignOut() {
try {
setLoading(true);
const { error } = await supabase.auth.signOut();
if (error) throw error;
await signOut();
} catch (error) {
if (error instanceof Error) {
Alert.alert('Fehler beim Abmelden', error.message);
@ -259,44 +209,30 @@ export default function SettingsScreen() {
/>
<Container>
<ScrollView className="flex-1 px-4 py-4">
{session && session.user ? (
{user ? (
<View style={styles.card}>
<Text style={styles.sectionTitle}>Mein Profil</Text>
<View style={styles.formGroup}>
<Text style={styles.label}>E-Mail</Text>
<Text style={styles.value}>{session?.user?.email}</Text>
<Text style={styles.value}>{user.email}</Text>
</View>
<View style={styles.formGroup}>
<Text style={styles.label}>Vorname</Text>
<Text style={styles.label}>Name</Text>
<TextInput
style={styles.input}
value={firstName}
onChangeText={setFirstName}
placeholder="Vorname eingeben"
value={name}
onChangeText={setName}
placeholder="Name eingeben"
placeholderTextColor={isDarkMode ? '#9CA3AF' : '#9CA3AF'}
/>
</View>
<View style={styles.formGroup}>
<Text style={styles.label}>Nachname</Text>
<TextInput
style={styles.input}
value={lastName}
onChangeText={setLastName}
placeholder="Nachname eingeben"
placeholderTextColor={isDarkMode ? '#9CA3AF' : '#9CA3AF'}
/>
</View>
{profile && profile.is_individual && (
{creditBalance !== null && (
<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>
<Text style={styles.quota}>{creditBalance}</Text>
</View>
)}
@ -308,7 +244,7 @@ export default function SettingsScreen() {
<TouchableOpacity
style={[styles.button, styles.signOutButton]}
onPress={signOut}
onPress={handleSignOut}
disabled={loading}
>
<Text style={styles.buttonText}>Abmelden</Text>

View file

@ -1,21 +1,14 @@
import React, { useState, useEffect } from 'react';
import { Stack, useRouter, useLocalSearchParams } from 'expo-router';
import {
ScrollView,
Text,
View,
TouchableOpacity,
Alert,
ActivityIndicator,
Modal,
} from 'react-native';
import { ScrollView, Text, View, TouchableOpacity } from 'react-native';
import { FontAwesome5 } from '@expo/vector-icons';
import { Container } from '~/components/Container';
import TeamMembers from '../../../components/TeamMembers';
import { useTheme } from '../../../utils/themeContext';
import { supabase } from '../../../utils/supabase';
import { refreshTeamList } from '../../../components/TeamList';
// TODO: Team-Details sind in mana-core-auth noch nicht implementiert.
// Diese Seite zeigt vorerst die vereinfachte TeamMembers-Komponente an.
export default function TeamDetails() {
const router = useRouter();
@ -25,8 +18,6 @@ export default function TeamDetails() {
}>();
const { isDarkMode } = useTheme();
const [teamName, setTeamName] = useState(initialTeamName || '');
const [deletingTeam, setDeletingTeam] = useState(false);
const [showDeleteModal, setShowDeleteModal] = useState(false);
useEffect(() => {
if (initialTeamName) {
@ -38,149 +29,6 @@ export default function TeamDetails() {
router.push('/teams');
};
const deleteTeam = () => {
console.log('Delete team button clicked, teamId:', teamId);
// Modal öffnen statt Alert anzeigen
setShowDeleteModal(true);
};
const confirmDelete = () => {
console.log('Löschen bestätigt für Team:', teamId);
setShowDeleteModal(false);
handleTeamDeletion();
};
const cancelDelete = () => {
console.log('Löschen abgebrochen');
setShowDeleteModal(false);
};
const handleTeamDeletion = async () => {
console.log('Starting deletion process for team:', teamId);
setDeletingTeam(true);
try {
// Zuerst alle Abhängigkeiten prüfen
console.log('Checking for dependencies...');
// 1. Prüfe auf credit_transactions
const { data: txData } = await supabase
.from('credit_transactions')
.select('id')
.eq('team_id', teamId);
console.log('Credit transactions:', txData);
// 2. Prüfe auf team_members
const { data: memberData } = await supabase
.from('team_members')
.select('user_id')
.eq('team_id', teamId);
console.log('Team members:', memberData);
// 3. Prüfe auf user_roles
const { data: roleData } = await supabase
.from('user_roles')
.select('id')
.eq('team_id', teamId);
console.log('User roles:', roleData);
// Jetzt versuchen wir, alle Abhängigkeiten zu löschen
// 1. Lösche credit_transactions
if (txData && txData.length > 0) {
console.log('Deleting credit transactions...');
const { error: txDeleteError } = await supabase
.from('credit_transactions')
.delete()
.eq('team_id', teamId);
if (txDeleteError) {
console.error('Error deleting credit transactions:', txDeleteError);
}
}
// 2. Lösche team_members
if (memberData && memberData.length > 0) {
console.log('Deleting team members...');
const { error: memberDeleteError } = await supabase
.from('team_members')
.delete()
.eq('team_id', teamId);
if (memberDeleteError) {
console.error('Error deleting team members:', memberDeleteError);
}
}
// 3. Lösche user_roles
if (roleData && roleData.length > 0) {
console.log('Deleting user roles...');
const { error: roleDeleteError } = await supabase
.from('user_roles')
.delete()
.eq('team_id', teamId);
if (roleDeleteError) {
console.error('Error deleting user roles:', roleDeleteError);
}
}
// 4. Schließlich das Team löschen
console.log('Deleting team...');
const { data, error } = await supabase.from('teams').delete().eq('id', teamId);
console.log('Team deletion response:', { data, error });
if (error) {
throw error;
}
console.log('Team successfully deleted');
// Teamliste aktualisieren, wenn der Benutzer authentifiziert ist
const {
data: { session },
} = await supabase.auth.getSession();
if (session?.user) {
// Teamliste aktualisieren
refreshTeamList(session.user.id, () => {
console.log('Team list refreshed after deletion');
});
}
// Erfolgsmeldung anzeigen und zurück zur Teamliste navigieren
Alert.alert('Erfolg', 'Das Team wurde erfolgreich gelöscht.', [
{
text: 'OK',
onPress: () => {
console.log('Navigating back to teams list');
// Zurück zur Teamliste navigieren
router.replace('/teams');
},
},
]);
// Auch ohne Klick auf OK zurück zur Teamliste navigieren (nach kurzer Verzögerung)
setTimeout(() => {
router.replace('/teams');
}, 1000);
} catch (error: any) {
console.error('Fehler beim Löschen des Teams:', error);
// Detaillierte Fehlermeldung anzeigen
Alert.alert(
'Fehler',
`Es ist ein Fehler beim Löschen des Teams aufgetreten: ${error?.message || JSON.stringify(error)}`,
[{ text: 'OK' }]
);
} finally {
setDeletingTeam(false);
}
};
return (
<>
<Stack.Screen
@ -190,65 +38,6 @@ export default function TeamDetails() {
}}
/>
{/* Lösch-Bestätigungsmodal */}
<Modal
animationType="fade"
transparent
visible={showDeleteModal}
onRequestClose={cancelDelete}
>
<View
className="flex-1 items-center justify-center"
style={{ backgroundColor: 'rgba(0,0,0,0.5)' }}
>
<View
className={`m-5 rounded-xl p-6 shadow-lg ${isDarkMode ? 'bg-gray-800' : 'bg-white'}`}
style={{ width: '80%' }}
>
<View className="mb-4 items-center">
<FontAwesome5 name="exclamation-triangle" size={40} color="#EF4444" />
</View>
<Text
className={`mb-2 text-center text-xl font-bold ${isDarkMode ? 'text-white' : 'text-gray-800'}`}
>
Team löschen
</Text>
<Text
className={`mb-6 text-center text-base ${isDarkMode ? 'text-gray-300' : 'text-gray-600'}`}
>
{`Möchtest du das Team "${teamName}" wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.`}
</Text>
<View className="flex-row justify-between">
<TouchableOpacity
className={`mr-2 flex-1 rounded-lg py-3 ${isDarkMode ? 'bg-gray-600' : 'bg-gray-200'}`}
onPress={cancelDelete}
>
<Text
className={`text-center font-medium ${isDarkMode ? 'text-white' : 'text-gray-800'}`}
>
Abbrechen
</Text>
</TouchableOpacity>
<TouchableOpacity
className="ml-2 flex-1 rounded-lg bg-red-600 py-3"
onPress={confirmDelete}
disabled={deletingTeam}
>
{deletingTeam ? (
<ActivityIndicator size="small" color="white" />
) : (
<Text className="text-center font-medium text-white">Löschen</Text>
)}
</TouchableOpacity>
</View>
</View>
</View>
</Modal>
<Container>
<ScrollView className="flex-1">
<View className="mx-2.5 my-2.5 flex-row justify-between">
@ -268,22 +57,6 @@ export default function TeamDetails() {
Zurück zu meinen Teams
</Text>
</TouchableOpacity>
<TouchableOpacity
className={`flex-row items-center rounded-lg px-4 py-2 ${deletingTeam ? 'bg-gray-500' : 'bg-red-600'}`}
onPress={deleteTeam} // Öffnet jetzt den Modal-Dialog
disabled={deletingTeam}
activeOpacity={0.7}
>
{deletingTeam ? (
<ActivityIndicator size="small" color="white" />
) : (
<>
<FontAwesome5 name="trash-alt" size={16} color="white" className="mr-2" />
<Text className="text-sm font-semibold text-white">Team löschen</Text>
</>
)}
</TouchableOpacity>
</View>
<TeamMembers teamId={teamId} />

View file

@ -1,64 +1,44 @@
import '../global.css';
import React, { useEffect, useState } from 'react';
import React, { useEffect } from 'react';
import { Stack, useRouter, useSegments } from 'expo-router';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { ThemeProvider } from '~/utils/themeContext';
import { supabase } from '../utils/supabase';
import { Session } from '@supabase/supabase-js';
import { AuthProvider, useAuth } from '~/context/AuthProvider';
export const unstable_settings = {
// Ensure that reloading on `/modal` keeps a back button present.
initialRouteName: '(drawer)',
};
// Auth Provider Komponente für die Authentifizierungsprüfung
function AuthProvider({ children }: { children: React.ReactNode }) {
const [session, setSession] = useState<Session | null>(null);
const [isLoading, setIsLoading] = useState(true);
// Navigation guard that redirects based on auth state
function NavigationGuard({ children }: { children: React.ReactNode }) {
const { user, loading } = useAuth();
const segments = useSegments();
const router = useRouter();
useEffect(() => {
// Prüfe den aktuellen Authentifizierungsstatus
supabase.auth.getSession().then(({ data: { session } }) => {
setSession(session);
setIsLoading(false);
});
// Abonniere Authentifizierungsänderungen
const {
data: { subscription },
} = supabase.auth.onAuthStateChange((_event, session) => {
setSession(session);
setIsLoading(false);
});
return () => subscription.unsubscribe();
}, []);
useEffect(() => {
if (isLoading) return;
if (loading) return;
// Prüfe, ob der Benutzer auf der Anmeldeseite oder Passwort-Reset-Seite ist
const isLoginScreen = segments[0] === 'login';
const isAuthScreen = segments[0] === 'auth'; // For reset-password and other auth routes
const isPublicScreen = isLoginScreen || isAuthScreen;
if (!session && !isPublicScreen) {
if (!user && !isPublicScreen) {
// Wenn der Benutzer nicht angemeldet ist und nicht auf einer öffentlichen Seite ist,
// leite ihn zur Anmeldeseite um
router.replace('/login');
} else if (session && isLoginScreen) {
} else if (user && isLoginScreen) {
// Wenn der Benutzer angemeldet ist und auf der Anmeldeseite ist,
// leite ihn zur Hauptseite um
router.replace('/');
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [session, segments, isLoading]);
}, [user, segments, loading]);
// Zeige nichts während des Ladens
if (isLoading) return null;
if (loading) return null;
return <>{children}</>;
}
@ -67,14 +47,16 @@ export default function RootLayout() {
return (
<ThemeProvider>
<AuthProvider>
<GestureHandlerRootView style={{ flex: 1 }}>
<Stack>
<Stack.Screen name="(drawer)" options={{ headerShown: false }} />
<Stack.Screen name="modal" options={{ title: 'Modal', presentation: 'modal' }} />
<Stack.Screen name="login" options={{ headerShown: true }} />
<Stack.Screen name="auth" options={{ headerShown: false }} />
</Stack>
</GestureHandlerRootView>
<NavigationGuard>
<GestureHandlerRootView style={{ flex: 1 }}>
<Stack>
<Stack.Screen name="(drawer)" options={{ headerShown: false }} />
<Stack.Screen name="modal" options={{ title: 'Modal', presentation: 'modal' }} />
<Stack.Screen name="login" options={{ headerShown: true }} />
<Stack.Screen name="auth" options={{ headerShown: false }} />
</Stack>
</GestureHandlerRootView>
</NavigationGuard>
</AuthProvider>
</ThemeProvider>
);

View file

@ -109,10 +109,8 @@ export default function ResetPasswordScreen() {
setLoading(true);
try {
const apiUrl =
process.env.EXPO_PUBLIC_API_URL ||
'https://mana-core-middleware-111768794939.europe-west3.run.app';
const endpoint = `${apiUrl}/auth/update-password`;
const apiUrl = process.env.EXPO_PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001';
const endpoint = `${apiUrl}/api/v1/auth/reset-password`;
console.log('Calling update password endpoint:', endpoint);

View file

@ -1,48 +1,36 @@
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';
import { useAuth } from '../context/AuthProvider';
import { api } from '../services/api';
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 }) {
export default function Account() {
const { user, signOut } = useAuth();
const [loading, setLoading] = useState(true);
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [profile, setProfile] = useState<Profile | null>(null);
const [name, setName] = useState('');
const [creditBalance, setCreditBalance] = useState<number | null>(null);
useEffect(() => {
if (session) getProfile();
if (user) loadProfile();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [session]);
}, [user]);
async function getProfile() {
async function loadProfile() {
try {
setLoading(true);
const { user } = session;
const { data, error } = await supabase
.from('profiles')
.select('*')
.eq('id', user.id)
.single();
const { data, error } = await api.getProfile();
if (error) {
throw error;
throw new Error(error);
}
if (data) {
setProfile(data);
setFirstName(data.first_name || '');
setLastName(data.last_name || '');
setName(data.name || '');
}
// Also load credit balance
const { data: creditData } = await api.getCreditBalance();
if (creditData) {
setCreditBalance(creditData.balance);
}
} catch (error) {
if (error instanceof Error) {
@ -56,35 +44,11 @@ export default function Account({ session }: { session: Session }) {
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,
},
]);
const { error } = await api.updateProfile({ name });
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;
if (error) {
throw new Error(error);
}
Alert.alert('Erfolg', 'Profil erfolgreich aktualisiert!');
@ -97,11 +61,10 @@ export default function Account({ session }: { session: Session }) {
}
}
async function signOut() {
async function handleSignOut() {
try {
setLoading(true);
const { error } = await supabase.auth.signOut();
if (error) throw error;
await signOut();
} catch (error) {
if (error instanceof Error) {
Alert.alert('Fehler beim Abmelden', error.message);
@ -118,35 +81,23 @@ export default function Account({ session }: { session: Session }) {
<View style={styles.formGroup}>
<Text style={styles.label}>E-Mail</Text>
<Text style={styles.value}>{session?.user?.email}</Text>
<Text style={styles.value}>{user?.email}</Text>
</View>
<View style={styles.formGroup}>
<Text style={styles.label}>Vorname</Text>
<Text style={styles.label}>Name</Text>
<TextInput
style={styles.input}
value={firstName}
onChangeText={setFirstName}
placeholder="Vorname eingeben"
value={name}
onChangeText={setName}
placeholder="Name 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 && (
{creditBalance !== null && (
<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>
<Text style={styles.quota}>{creditBalance}</Text>
</View>
)}
@ -158,7 +109,7 @@ export default function Account({ session }: { session: Session }) {
<TouchableOpacity
style={[styles.button, styles.signOutButton]}
onPress={signOut}
onPress={handleSignOut}
disabled={loading}
>
<Text style={styles.buttonText}>Abmelden</Text>

View file

@ -1,12 +1,13 @@
import React, { useState } from 'react';
import { Alert, StyleSheet, View, TextInput, TouchableOpacity, Text, Platform } from 'react-native';
import { FontAwesome5 } from '@expo/vector-icons';
import { supabase } from '../utils/supabase';
import { useAuth } from '../context/AuthProvider';
import { useTheme, useThemeColors } from '../utils/themeContext';
export default function Auth() {
const { isDarkMode } = useTheme();
const themeColors = useThemeColors();
const { signIn, signUp, resetPassword } = useAuth();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
@ -15,38 +16,30 @@ export default function Auth() {
async function signInWithEmail() {
setLoading(true);
const { error } = await supabase.auth.signInWithPassword({
email,
password,
});
const { error } = await signIn(email, password);
if (error) {
Alert.alert('Fehler bei der Anmeldung', error.message);
Alert.alert('Fehler bei der Anmeldung', error.message || 'Anmeldung fehlgeschlagen');
}
setLoading(false);
}
async function signUpWithEmail() {
setLoading(true);
const { error } = await supabase.auth.signUp({
email,
password,
});
const { error } = await signUp(email, password);
if (error) {
Alert.alert('Fehler bei der Registrierung', error.message);
Alert.alert('Fehler bei der Registrierung', error.message || 'Registrierung fehlgeschlagen');
} else {
Alert.alert(
'Registrierung erfolgreich',
'Bitte überprüfen Sie Ihre E-Mail für den Bestätigungslink.'
'Sie wurden erfolgreich registriert und angemeldet.'
);
}
setLoading(false);
}
async function resetPassword() {
console.log('Reset password called with email:', email);
async function handleResetPassword() {
if (!email) {
Alert.alert('Fehler', 'Bitte geben Sie Ihre E-Mail-Adresse ein');
return;
@ -55,39 +48,18 @@ export default function Auth() {
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`;
const { error } = await resetPassword(email);
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');
if (error) {
Alert.alert('Fehler', error.message || 'Fehler beim Zurücksetzen des Passworts');
} else {
Alert.alert(
'E-Mail gesendet',
'Bitte überprüfen Sie Ihre E-Mail für den Link zum Zurücksetzen des Passworts.'
);
setIsResetPassword(false);
}
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.'
@ -189,7 +161,7 @@ export default function Auth() {
]}
onPress={() => {
if (isResetPassword) {
resetPassword();
handleResetPassword();
} else if (isSignUp) {
signUpWithEmail();
} else {

View file

@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState } from 'react';
import {
View,
Text,
@ -8,168 +8,63 @@ import {
ActivityIndicator,
ScrollView,
} from 'react-native';
import { supabase } from '../utils/supabase';
import { Session } from '@supabase/supabase-js';
import { useAuth } from '../context/AuthProvider';
import { api } from '../services/api';
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 { user } = useAuth();
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) {
if (!user) {
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();
const { data, error } = await api.createOrganization({
name: organizationName.trim(),
});
if (orgError) throw orgError;
if (error) {
throw new Error(error);
}
// 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);
if (onOrgCreated && data) {
const orgData = data as any;
onOrgCreated(orgData.id || orgData.organizationId, organizationName.trim());
}
// Formular zurücksetzen
setOrganizationName('');
setInitialCredits('');
} catch (error) {
} catch (error: any) {
console.error('Fehler beim Erstellen der Organisation:', error);
Alert.alert('Fehler', 'Es ist ein Fehler beim Erstellen der Organisation aufgetreten.');
Alert.alert(
'Fehler',
error.message || 'Es ist ein Fehler beim Erstellen der Organisation aufgetreten.'
);
} finally {
setLoading(false);
}
}
if (!session) {
if (!user) {
return (
<View className={`${isDarkMode ? 'bg-gray-800' : 'bg-white'} m-2.5 rounded-lg p-5 shadow`}>
<Text
@ -181,32 +76,6 @@ export default function CreateOrganization({ onOrgCreated }: CreateOrganizationP
);
}
if (checkingPermission) {
return (
<View className={`${isDarkMode ? 'bg-gray-800' : 'bg-white'} m-2.5 rounded-lg p-5 shadow`}>
<ActivityIndicator size="large" color={isDarkMode ? '#93C5FD' : '#0055FF'} />
<Text
className={`text-base ${isDarkMode ? 'text-gray-300' : 'text-gray-600'} mt-2.5 text-center`}
>
Prüfe Berechtigungen...
</Text>
</View>
);
}
if (!userHasPermission) {
return (
<View className={`${isDarkMode ? 'bg-gray-800' : 'bg-white'} m-2.5 rounded-lg p-5 shadow`}>
<Text
className={`text-base ${isDarkMode ? 'text-gray-300' : 'text-gray-600'} p-5 text-center`}
>
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'} m-2.5 rounded-lg p-5 shadow`}>
@ -231,26 +100,6 @@ export default function CreateOrganization({ onOrgCreated }: CreateOrganizationP
/>
</View>
<View className="mb-4">
<Text
className={`mb-2 text-base font-medium ${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={`mt-1 text-sm ${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'} rounded-lg px-4 py-3 ${loading ? 'opacity-70' : ''}`}
onPress={createOrganization}
@ -268,5 +117,3 @@ export default function CreateOrganization({ onOrgCreated }: CreateOrganizationP
</ScrollView>
);
}
// NativeWind wird für das Styling verwendet, daher sind keine StyleSheet-Definitionen erforderlich

View file

@ -1,243 +1,21 @@
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 React from 'react';
import { View, Text } from 'react-native';
import { FontAwesome5 } from '@expo/vector-icons';
import { useAuth } from '../context/AuthProvider';
import { useTheme } from '../utils/themeContext';
import { useRouter } from 'expo-router';
interface Organization {
id: string;
name: string;
}
interface UserRole {
organization_id: string;
roles?: {
name: string;
};
}
// TODO: Team-Erstellung ist in mana-core-auth noch nicht implementiert.
// Diese Komponente zeigt vorerst einen Hinweis an.
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);
export default function CreateTeam({ onTeamCreated: _onTeamCreated }: CreateTeamProps) {
const { isDarkMode } = useTheme();
const { user } = useAuth();
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) {
if (!user) {
return (
<View className={`${isDarkMode ? 'bg-gray-800' : 'bg-white'} m-2.5 rounded-lg p-5 shadow`}>
<Text
@ -249,108 +27,21 @@ export default function CreateTeam({ onTeamCreated }: CreateTeamProps) {
);
}
if (fetchingOrgs) {
return (
<View className={`${isDarkMode ? 'bg-gray-800' : 'bg-white'} m-2.5 rounded-lg p-5 shadow`}>
<ActivityIndicator size="large" color={isDarkMode ? '#93C5FD' : '#0055FF'} />
<Text
className={`text-base ${isDarkMode ? 'text-gray-300' : 'text-gray-600'} mt-2.5 text-center`}
>
Lade Organisationen...
</Text>
</View>
);
}
if (organizations.length === 0) {
return (
<View className={`${isDarkMode ? 'bg-gray-800' : 'bg-white'} m-2.5 rounded-lg p-5 shadow`}>
<Text
className={`text-base ${isDarkMode ? 'text-gray-300' : 'text-gray-600'} p-5 text-center`}
>
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'} m-2.5 rounded-lg p-5 shadow`}>
<View className={`${isDarkMode ? 'bg-gray-800' : 'bg-white'} m-2.5 rounded-lg p-5 shadow`}>
<Text className={`mb-5 text-2xl font-bold ${isDarkMode ? 'text-gray-100' : 'text-gray-800'}`}>
Neues Team erstellen
</Text>
<View className="items-center py-8">
<FontAwesome5 name="users" size={50} color={isDarkMode ? '#4B5563' : '#9CA3AF'} />
<Text
className={`mb-5 text-2xl font-bold ${isDarkMode ? 'text-gray-100' : 'text-gray-800'}`}
className={`mt-4 text-center text-base ${isDarkMode ? 'text-gray-300' : 'text-gray-600'}`}
>
Neues Team erstellen
Die Team-Erstellung wird derzeit auf das neue Auth-System migriert und ist bald wieder
verfügbar.
</Text>
<View className="mb-4">
<Text
className={`mb-2 text-base font-medium ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}
>
Organisation
</Text>
<View className="mb-2.5 flex-row flex-wrap">
{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'} mb-2.5 mr-2.5 rounded-lg px-4 py-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={`mb-2 text-base font-medium ${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={`mb-2 text-base font-medium ${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={`mt-2.5 h-12 items-center justify-center rounded-lg ${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-base font-bold text-white">Team erstellen</Text>
)}
</TouchableOpacity>
</View>
</ScrollView>
</View>
);
}
// NativeWind wird für das Styling verwendet, daher sind keine StyleSheet-Definitionen erforderlich

View file

@ -2,89 +2,41 @@ 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 { useAuth } from '../context/AuthProvider';
import { api } from '../services/api';
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 { user } = useAuth();
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);
}
});
if (user) {
fetchUserStats();
} else {
setLoading(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [user]);
// 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) {
async function fetchUserStats() {
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);
// Fetch organizations count
const { data: orgsData } = await api.getOrganizations();
setOrgCount(orgsData?.length || 0);
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);
// Fetch credit balance
const { data: creditData } = await api.getCreditBalance();
if (creditData) {
setTotalMana(creditData.totalCredits || 0);
setAvailableMana(creditData.balance || 0);
}
} catch (error) {
console.error('Fehler beim Abrufen der Benutzerstatistiken:', error);
@ -93,7 +45,7 @@ export default function DashboardStats() {
}
}
if (!session || loading) {
if (!user || loading) {
return (
<View
className={`flex-row justify-between ${isDarkMode ? 'bg-gray-800' : 'bg-white'} mb-5 rounded-lg p-4 shadow`}
@ -135,40 +87,12 @@ export default function DashboardStats() {
</TouchableOpacity>
</View>
{/* Teams und Organisationen */}
{/* 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'} mr-2 rounded-lg p-3`}
onPress={() => router.push('/teams')}
>
<View className="items-center">
<View className="mb-1 flex-row items-center">
<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'} rounded-full px-3 py-1`}
>
<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'} ml-2 rounded-lg p-3`}
className={`flex-1 flex-row items-center justify-center ${isDarkMode ? 'bg-gray-700' : 'bg-gray-50'} rounded-lg p-3`}
onPress={() => router.push('/organizations')}
>
<View className="items-center">

View file

@ -1,19 +1,18 @@
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 { useAuth } from '../context/AuthProvider';
import { api } from '../services/api';
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;
slug: string;
logo?: string;
createdAt: string;
metadata?: Record<string, any>;
}
interface OrganizationListProps {
@ -27,7 +26,7 @@ interface OrganizationListRef {
const OrganizationList = forwardRef<OrganizationListRef, OrganizationListProps>(
({ hideTitle = false }, ref) => {
const [session, setSession] = useState<Session | null>(null);
const { user } = useAuth();
const [loading, setLoading] = useState(true);
const [organizations, setOrganizations] = useState<Organization[]>([]);
const { isDarkMode } = useTheme();
@ -36,110 +35,32 @@ const OrganizationList = forwardRef<OrganizationListRef, OrganizationListProps>(
// Stelle die refreshOrganizations-Methode über die Ref zur Verfügung
useImperativeHandle(ref, () => ({
refreshOrganizations: () => {
if (session) {
console.log('Aktualisiere Organisationsliste');
fetchUserOrganizations(session.user.id);
if (user) {
fetchUserOrganizations();
}
},
}));
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();
if (user) {
fetchUserOrganizations();
} else {
setLoading(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
}, [user]);
async function fetchUserOrganizations(userId: string) {
async function fetchUserOrganizations() {
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;
};
const { data, error } = await api.getOrganizations();
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([]);
if (error) {
throw new Error(error);
}
setOrganizations(data || []);
} catch (error) {
console.error('Fehler beim Abrufen der Organisationen:', error);
Alert.alert('Fehler', 'Es ist ein Fehler beim Laden Ihrer Organisationen aufgetreten.');
@ -148,33 +69,6 @@ const OrganizationList = forwardRef<OrganizationListRef, OrganizationListProps>(
}
}
// 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', {
@ -184,22 +78,7 @@ const OrganizationList = forwardRef<OrganizationListRef, OrganizationListProps>(
});
};
// 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) {
if (!user) {
return (
<View
className={`flex-1 ${isDarkMode ? 'bg-gray-800' : 'bg-white'} m-2.5 rounded-lg p-4 shadow`}
@ -271,7 +150,6 @@ const OrganizationList = forwardRef<OrganizationListRef, OrganizationListProps>(
renderItem={({ item }) => (
<TouchableOpacity
onPress={() => {
console.log('Organisation angeklickt:', item.id, item.name);
router.push({
pathname: '/organizations/[id]',
params: { id: item.id, name: item.name },
@ -295,85 +173,19 @@ const OrganizationList = forwardRef<OrganizationListRef, OrganizationListProps>(
{item.name}
</Text>
</View>
<View
className={`rounded-full px-2 py-0.5 ${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="mb-3 flex-row items-center justify-between">
<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={`mt-1 flex-row justify-between border-t pt-2 ${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>
{/* Erstellungsdatum */}
<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.createdAt)}
</Text>
</View>
</TouchableOpacity>
)}
@ -384,7 +196,4 @@ const OrganizationList = forwardRef<OrganizationListRef, OrganizationListProps>(
}
);
// NativeWind wird für das Styling verwendet, daher sind keine StyleSheet-Definitionen erforderlich
// Exportiere die Komponente mit forwardRef
export default OrganizationList;

View file

@ -1,91 +1,29 @@
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 { View, Text, StyleSheet } from 'react-native';
import { useAuth } from '../context/AuthProvider';
import { api } from '../services/api';
import { useTheme } from '../utils/themeContext';
interface User {
id: string;
email: string;
first_name?: string;
last_name?: string;
}
// TODO: Mana-Übertragung zwischen Benutzern ist in mana-core-auth noch nicht implementiert.
// Diese Komponente zeigt vorerst nur den aktuellen Kontostand an.
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 { user } = useAuth();
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);
}
});
if (user) {
fetchUserCredits();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [user]);
// 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) {
async function fetchUserCredits() {
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);
const { data } = await api.getCreditBalance();
if (data) {
setUserCredits(data.balance || 0);
}
} catch (error) {
console.error('Fehler beim Abrufen der Kredite:', error);
@ -93,151 +31,6 @@ export default function SendMana() {
}
}
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: {
@ -276,102 +69,21 @@ export default function SendMana() {
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,
},
comingSoon: {
fontSize: 14,
color: isDarkMode ? '#9CA3AF' : '#666',
textAlign: 'center',
fontStyle: 'italic',
},
});
if (!session) {
if (!user) {
return (
<View style={styles.container}>
<Text style={styles.notLoggedIn}>Bitte melden Sie sich an, um Mana zu senden.</Text>
@ -388,79 +100,9 @@ export default function SendMana() {
<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>
<Text style={styles.comingSoon}>
Die Mana-Übertragung zwischen Benutzern wird bald verfügbar sein.
</Text>
</View>
);
}

View file

@ -1,257 +1,27 @@
import React, { useState, useEffect } from 'react';
import {
View,
Text,
FlatList,
TouchableOpacity,
ActivityIndicator,
Alert,
Platform,
} from 'react-native';
import React from 'react';
import { View, Text } 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 { useAuth } from '../context/AuthProvider';
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;
}
// TODO: Teams mit Credit-Zuweisung sind in mana-core-auth noch nicht implementiert.
// Diese Komponente zeigt vorerst einen Hinweis an.
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();
}
});
// Stub for external callers that used refreshTeamList
export function refreshTeamList(_userId: string, callback?: () => void) {
// No-op: Teams are not yet available via mana-core-auth
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();
const { user } = useAuth();
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) {
if (!user) {
return (
<Text
className={`text-base ${isDarkMode ? 'text-gray-300' : 'text-gray-600'} mx-2.5 p-5 text-center`}
@ -261,43 +31,6 @@ export default function TeamList({ hideTitle = false }: TeamListProps) {
);
}
if (loading) {
return (
<View className="mx-2.5 flex-1 items-center justify-center p-4">
<ActivityIndicator size="large" color="#0055FF" />
<Text
className={`text-base ${isDarkMode ? 'text-gray-300' : 'text-gray-600'} mt-2.5 text-center`}
>
Lade Teams...
</Text>
</View>
);
}
if (teams.length === 0) {
return (
<View className="mx-2.5 flex-1 items-center justify-center p-4">
<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'} mb-2.5 text-center`}
>
Sie sind derzeit kein Mitglied in einem Team.
</Text>
<Text
className={`text-sm ${isDarkMode ? 'text-gray-300' : 'text-gray-600'} px-5 text-center`}
>
Erstellen Sie ein neues Team oder bitten Sie einen Administrator, Sie einem Team
hinzuzufügen.
</Text>
</View>
);
}
return (
<>
{!hideTitle && (
@ -307,109 +40,24 @@ export default function TeamList({ hideTitle = false }: TeamListProps) {
Meine Teams
</Text>
)}
<FlatList
data={teams}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<TouchableOpacity
className={`${isDarkMode ? 'bg-gray-700' : 'bg-gray-50'} mb-2.5 rounded-lg p-4 shadow-sm`}
style={Platform.OS === 'web' ? { cursor: 'pointer' } : undefined}
onPress={() => navigateToTeamDetails(item.id, item.name)}
activeOpacity={0.7}
>
{/* Header mit Teamname */}
<View className="mb-3 flex-row items-center justify-between">
<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="mb-3 flex-row items-center justify-between">
<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={`mt-1 flex-row justify-between border-t pt-2 ${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="px-2.5 pb-5"
/>
<View className="mx-2.5 flex-1 items-center justify-center p-4">
<FontAwesome5
name="users"
size={50}
color={isDarkMode ? '#4B5563' : '#ccc'}
className="mb-4"
/>
<Text
className={`text-lg font-medium ${isDarkMode ? 'text-gray-100' : 'text-gray-800'} mb-2.5 text-center`}
>
Teams werden bald verfügbar sein.
</Text>
<Text
className={`text-sm ${isDarkMode ? 'text-gray-300' : 'text-gray-600'} px-5 text-center`}
>
Die Team-Verwaltung mit Credit-Zuweisung wird derzeit auf das neue Auth-System migriert.
</Text>
</View>
</>
);
}
// NativeWind wird für das Styling verwendet, daher sind keine StyleSheet-Definitionen erforderlich

View file

@ -1,464 +1,21 @@
import React, { useState, useEffect } from 'react';
import {
View,
Text,
FlatList,
TouchableOpacity,
ActivityIndicator,
Alert,
TextInput,
} from 'react-native';
import React from 'react';
import { View, Text } from 'react-native';
import { FontAwesome5 } from '@expo/vector-icons';
import { supabase } from '../utils/supabase';
import { Session } from '@supabase/supabase-js';
import { useAuth } from '../context/AuthProvider';
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;
}
// TODO: Team-Mitgliederverwaltung mit Credit-Zuweisung ist in mana-core-auth noch nicht implementiert.
// Diese Komponente zeigt vorerst einen Hinweis an.
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);
export default function TeamMembers({ teamId: _teamId }: TeamMembersProps) {
const { user } = useAuth();
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();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [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) {
if (!user) {
return (
<View
className={`flex-1 ${isDarkMode ? 'bg-gray-800' : 'bg-white'} m-2.5 rounded-lg p-4 shadow`}
@ -472,106 +29,8 @@ export default function TeamMembers({ teamId }: TeamMembersProps) {
);
}
if (loading) {
return (
<View
className={`flex-1 ${isDarkMode ? 'bg-gray-800' : 'bg-white'} m-2.5 rounded-lg p-4 shadow`}
>
<ActivityIndicator size="large" color="#0055FF" />
<Text
className={`text-base ${isDarkMode ? 'text-gray-300' : 'text-gray-600'} mt-2.5 text-center`}
>
Lade Teammitglieder...
</Text>
</View>
);
}
return (
<View className="flex-1">
{teamDetails && (
<View className={`${isDarkMode ? 'bg-gray-800' : 'bg-white'} m-2.5 rounded-lg p-4 shadow`}>
{isEditing ? (
<View>
<View className="mb-3 flex-row items-center">
<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 ? 'border-gray-600 bg-gray-700 text-white' : 'border-gray-300 bg-gray-50 text-gray-800'} mr-2 rounded-lg border px-3 py-2`}
value={newTeamName}
onChangeText={setNewTeamName}
placeholder="Teamname"
placeholderTextColor={isDarkMode ? '#9CA3AF' : '#6B7280'}
/>
</View>
<View className="mb-2 mt-3 flex-row justify-end">
<TouchableOpacity
className={`mr-2 flex-row items-center rounded-lg px-4 py-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 rounded-lg px-4 py-2 ${isDarkMode ? 'bg-blue-700' : 'bg-blue-600'}`}
onPress={updateTeamName}
disabled={updatingName}
>
{updatingName ? (
<Text className="font-semibold text-white">Speichern...</Text>
) : (
<Text className="font-semibold text-white">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="mb-1 flex-row items-center justify-between">
<Text
className={`text-xl font-bold ${isDarkMode ? 'text-gray-100' : 'text-gray-800'}`}
>
{teamDetails.name}
</Text>
{isAdmin && (
<TouchableOpacity
className={`rounded-full p-2 ${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'} m-2.5 rounded-lg p-4 shadow`}>
<Text
className={`text-lg font-semibold ${isDarkMode ? 'text-gray-100' : 'text-gray-800'} mb-4`}
@ -579,163 +38,15 @@ export default function TeamMembers({ teamId }: TeamMembersProps) {
Teammitglieder
</Text>
{isAdmin && (
<View className={`mb-4 ${isDarkMode ? 'bg-gray-700' : 'bg-gray-50'} rounded-lg p-3`}>
<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 ? 'border-gray-500 bg-gray-600 text-white' : 'border-gray-300 bg-white text-gray-800'} mr-2 rounded-lg border px-3 py-2`}
placeholder="E-Mail-Adresse"
placeholderTextColor={isDarkMode ? '#9CA3AF' : '#6B7280'}
value={newMemberEmail}
onChangeText={setNewMemberEmail}
autoCapitalize="none"
keyboardType="email-address"
/>
<TouchableOpacity
className="items-center justify-center rounded-lg bg-blue-600 px-4 py-2"
onPress={inviteMember}
disabled={inviting}
>
{inviting ? (
<ActivityIndicator size="small" color="white" />
) : (
<Text className="font-medium text-white">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'} mt-4 text-center`}
>
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'} mb-2 rounded-lg p-3`}
>
<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="mt-1 flex-row items-center">
{editingMemberId === item.user_id ? (
<View className="mr-4 flex-row items-center">
<Text
className={`text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-500'} mr-2`}
>
Credits:
</Text>
<TextInput
className={`${isDarkMode ? 'border-gray-500 bg-gray-600 text-white' : 'border-gray-300 bg-white text-gray-800'} w-16 rounded border px-2 py-1 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={`mr-4 flex-row items-center ${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'} mr-2 rounded-full p-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'} rounded-full p-2`}
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 className="items-center justify-center py-8">
<FontAwesome5 name="users" size={50} color={isDarkMode ? '#4B5563' : '#ccc'} />
<Text
className={`text-base ${isDarkMode ? 'text-gray-300' : 'text-gray-600'} mt-4 text-center`}
>
Die Team-Mitgliederverwaltung wird derzeit auf das neue Auth-System migriert und ist
bald wieder verfügbar.
</Text>
</View>
</View>
</View>
);

View file

@ -0,0 +1,228 @@
import React, { createContext, useContext, useEffect, useState } from 'react';
import { ActivityIndicator, View, Text } from 'react-native';
import * as SecureStore from 'expo-secure-store';
import {
createAuthService,
createTokenManager,
setStorageAdapter,
setDeviceAdapter,
setNetworkAdapter,
createMemoryStorageAdapter,
type UserData,
} from '@manacore/shared-auth';
// Mana Core Auth URL from environment
const MANA_AUTH_URL = process.env.EXPO_PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001';
// Create SecureStore adapter for React Native
const createSecureStoreAdapter = () => ({
async getItem<T>(key: string): Promise<T | null> {
try {
const value = await SecureStore.getItemAsync(key);
return value ? JSON.parse(value) : null;
} catch {
return null;
}
},
async setItem(key: string, value: unknown): Promise<void> {
await SecureStore.setItemAsync(key, JSON.stringify(value));
},
async removeItem(key: string): Promise<void> {
await SecureStore.deleteItemAsync(key);
},
});
// Create device adapter for React Native
const createReactNativeDeviceAdapter = () => {
let deviceId: string | null = null;
return {
async getDeviceInfo() {
if (!deviceId) {
// Try to get stored device ID
deviceId = await SecureStore.getItemAsync('@device/id');
if (!deviceId) {
// Generate new device ID
deviceId = `rn-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
await SecureStore.setItemAsync('@device/id', deviceId);
}
}
return {
deviceId,
deviceName: 'React Native Device',
platform: 'react-native',
};
},
async getStoredDeviceId() {
return deviceId || (await SecureStore.getItemAsync('@device/id'));
},
};
};
// Create network adapter (basic implementation)
const createReactNativeNetworkAdapter = () => ({
async isConnected() {
return true; // Always assume connected for now
},
async hasStableConnection() {
return true;
},
});
// Initialize adapters
setStorageAdapter(createSecureStoreAdapter());
setDeviceAdapter(createReactNativeDeviceAdapter());
setNetworkAdapter(createReactNativeNetworkAdapter());
// Create auth service
const authService = createAuthService({ baseUrl: MANA_AUTH_URL });
const tokenManager = createTokenManager(authService);
// Auth context type
type AuthContextType = {
user: UserData | null;
loading: boolean;
signIn: (email: string, password: string) => Promise<{ error: any | null }>;
signUp: (email: string, password: string) => Promise<{ error: any | null; data: any | null }>;
signOut: () => Promise<void>;
resetPassword: (email: string) => Promise<{ error: any | null }>;
};
// Create auth context
const AuthContext = createContext<AuthContextType | undefined>(undefined);
// Hook to access auth context
export const useAuth = () => {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};
// AuthProvider component
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<UserData | null>(null);
const [loading, setLoading] = useState(true);
// Initialize auth state
useEffect(() => {
const initialize = async () => {
try {
setLoading(true);
// Check if user is authenticated
const authenticated = await authService.isAuthenticated();
if (authenticated) {
const userData = await authService.getUserFromToken();
setUser(userData);
}
} catch (error) {
console.error('Fehler beim Initialisieren der Auth-Session:', error);
setUser(null);
} finally {
setLoading(false);
}
};
initialize();
}, []);
// Sign in with email and password
const signIn = async (email: string, password: string) => {
try {
const result = await authService.signIn(email, password);
if (!result.success) {
return { error: { message: result.error } };
}
// Get user data from token
const userData = await authService.getUserFromToken();
setUser(userData);
return { error: null };
} catch (error: any) {
console.error('Fehler beim Anmelden:', error.message || error);
return { error };
}
};
// Sign up with email and password
const signUp = async (email: string, password: string) => {
try {
const result = await authService.signUp(email, password);
if (!result.success) {
return { data: null, error: { message: result.error } };
}
// Auto sign in after successful signup
const signInResult = await signIn(email, password);
if (signInResult.error) {
return { data: null, error: signInResult.error };
}
return { data: user, error: null };
} catch (error) {
console.error('Fehler beim Registrieren:', error);
return { data: null, error };
}
};
// Sign out
const signOut = async () => {
try {
await authService.signOut();
setUser(null);
} catch (error) {
console.error('Fehler beim Abmelden:', error);
}
};
// Reset password
const resetPassword = async (email: string) => {
try {
const result = await authService.forgotPassword(email);
if (!result.success) {
return { error: { message: result.error } };
}
return { error: null };
} catch (error) {
console.error('Fehler beim Zurücksetzen des Passworts:', error);
return { error };
}
};
// Show loading indicator during initialization
if (loading) {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<ActivityIndicator size="large" color="#F59E0B" />
<Text style={{ marginTop: 16 }}>Authentifizierung wird initialisiert...</Text>
</View>
);
}
// Provide auth context
return (
<AuthContext.Provider
value={{
user,
loading,
signIn,
signUp,
signOut,
resetPassword,
}}
>
{children}
</AuthContext.Provider>
);
}

View file

@ -21,8 +21,9 @@
"@react-navigation/bottom-tabs": "^7.0.5",
"@react-navigation/drawer": "^7.0.0",
"@react-navigation/native": "^7.0.3",
"@supabase/supabase-js": "^2.81.1",
"@manacore/shared-auth": "workspace:*",
"expo": "^54.0.25",
"expo-secure-store": "^15.0.7",
"expo-constants": "~18.0.10",
"expo-dev-client": "~6.0.18",
"expo-dev-launcher": "^5.0.17",

View file

@ -0,0 +1,209 @@
import * as SecureStore from 'expo-secure-store';
const BASE_URL = process.env.EXPO_PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001';
/**
* API client for mana-core-auth service.
* Wraps fetch calls and automatically attaches JWT from SecureStore.
*/
class ManaCoreApi {
private async getToken(): Promise<string | null> {
try {
const raw = await SecureStore.getItemAsync('@manacore/app_token');
if (!raw) return null;
// The token might be stored as a JSON string
try {
const parsed = JSON.parse(raw);
return typeof parsed === 'string' ? parsed : (parsed?.accessToken ?? raw);
} catch {
return raw;
}
} catch {
return null;
}
}
private async request<T>(
path: string,
options: RequestInit = {}
): Promise<{ data: T | null; error: string | null }> {
try {
const token = await this.getToken();
const headers: Record<string, string> = {
'Content-Type': 'application/json',
...(options.headers as Record<string, string>),
};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
const response = await fetch(`${BASE_URL}${path}`, {
...options,
headers,
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
return {
data: null,
error: errorData.message || errorData.error || `HTTP ${response.status}`,
};
}
// Handle 204 No Content
if (response.status === 204) {
return { data: null, error: null };
}
const data = await response.json();
return { data, error: null };
} catch (error: any) {
return {
data: null,
error: error.message || 'Netzwerkfehler. Bitte versuchen Sie es später erneut.',
};
}
}
// ---- Profile ----
async getProfile() {
return this.request<{
id: string;
email: string;
name: string;
image?: string;
createdAt: string;
}>('/api/v1/auth/profile');
}
async updateProfile(data: { name?: string; image?: string }) {
return this.request('/api/v1/auth/profile', {
method: 'POST',
body: JSON.stringify(data),
});
}
// ---- Organizations ----
async getOrganizations() {
return this.request<
Array<{
id: string;
name: string;
slug: string;
logo?: string;
createdAt: string;
metadata?: Record<string, any>;
}>
>('/api/v1/auth/organizations');
}
async getOrganization(id: string) {
return this.request<{
id: string;
name: string;
slug: string;
logo?: string;
createdAt: string;
metadata?: Record<string, any>;
}>(`/api/v1/auth/organizations/${id}`);
}
async createOrganization(data: { name: string; slug?: string }) {
return this.request('/api/v1/auth/register/b2b', {
method: 'POST',
body: JSON.stringify(data),
});
}
async deleteOrganization(id: string) {
return this.request(`/api/v1/auth/organizations/${id}`, {
method: 'DELETE',
});
}
// ---- Organization Members ----
async getOrgMembers(orgId: string) {
return this.request<
Array<{
id: string;
userId: string;
email?: string;
name?: string;
role: string;
createdAt: string;
}>
>(`/api/v1/auth/organizations/${orgId}/members`);
}
async inviteMember(orgId: string, email: string, role: string = 'member') {
return this.request(`/api/v1/auth/organizations/${orgId}/invite`, {
method: 'POST',
body: JSON.stringify({ email, role }),
});
}
async removeMember(orgId: string, memberId: string) {
return this.request(`/api/v1/auth/organizations/${orgId}/members/${memberId}`, {
method: 'DELETE',
});
}
async updateMemberRole(orgId: string, memberId: string, role: string) {
return this.request(`/api/v1/auth/organizations/${orgId}/members/${memberId}/role`, {
method: 'PATCH',
body: JSON.stringify({ role }),
});
}
// ---- Credits ----
async getCreditBalance() {
return this.request<{
balance: number;
totalCredits: number;
usedCredits: number;
}>('/api/v1/credits/balance');
}
async getCreditTransactions() {
return this.request<
Array<{
id: string;
amount: number;
description: string;
createdAt: string;
type: string;
}>
>('/api/v1/credits/transactions');
}
// ---- Invitations ----
async getInvitations() {
return this.request<
Array<{
id: string;
organizationId: string;
organizationName: string;
email: string;
role: string;
status: string;
expiresAt: string;
}>
>('/api/v1/auth/invitations');
}
async acceptInvitation(invitationId: string) {
return this.request('/api/v1/auth/organizations/accept-invitation', {
method: 'POST',
body: JSON.stringify({ invitationId }),
});
}
}
// Export a singleton instance
export const api = new ManaCoreApi();

View file

@ -1,121 +0,0 @@
/**
* Hybrid storage solution for Supabase authentication
* Uses localStorage in browser environments and memory storage in SSR
* This avoids issues with AsyncStorage during server-side rendering
*/
import AsyncStorage from '@react-native-async-storage/async-storage';
interface StorageData {
[key: string]: string;
}
class HybridStorageService {
private memoryStorage: StorageData = {};
private isAsyncStorageAvailable = false;
constructor() {
// Check if we're in an environment where AsyncStorage is available
this.checkAsyncStorageAvailability();
}
private async checkAsyncStorageAvailability(): Promise<void> {
try {
// Check if we're in a browser environment
if (typeof window === 'undefined') {
this.isAsyncStorageAvailable = false;
return;
}
// Try to access AsyncStorage
this.isAsyncStorageAvailable = typeof AsyncStorage !== 'undefined';
// If available, try to load any existing auth data into memory
if (this.isAsyncStorageAvailable) {
await this.syncFromAsyncStorage();
}
} catch (_error) {
console.warn('AsyncStorage not available, falling back to memory storage');
this.isAsyncStorageAvailable = false;
}
}
private async syncFromAsyncStorage(): Promise<void> {
if (typeof window === 'undefined') {
// Skip AsyncStorage sync in SSR environment
return;
}
try {
// Get all keys that start with 'supabase.auth'
const allKeys = await AsyncStorage.getAllKeys();
const authKeys = allKeys.filter((key) => key.startsWith('supabase.auth'));
if (authKeys.length > 0) {
const keyValuePairs = await AsyncStorage.multiGet(authKeys);
keyValuePairs.forEach(([key, value]) => {
if (value) {
this.memoryStorage[key] = value;
}
});
}
} catch (error) {
console.error('Error syncing from AsyncStorage:', error);
}
}
async getItem(key: string): Promise<string | null> {
// First check memory storage
const memoryValue = this.memoryStorage[key];
if (memoryValue) return memoryValue;
// If not in memory and AsyncStorage is available, try to get from there
if (this.isAsyncStorageAvailable && typeof window !== 'undefined') {
try {
const value = await AsyncStorage.getItem(key);
if (value) {
// Update memory cache
this.memoryStorage[key] = value;
return value;
}
} catch (error) {
console.error('Error reading from AsyncStorage:', error);
}
}
return null;
}
async setItem(key: string, value: string): Promise<void> {
// Always update memory storage
this.memoryStorage[key] = value;
// If AsyncStorage is available, also persist there
if (this.isAsyncStorageAvailable && typeof window !== 'undefined') {
try {
await AsyncStorage.setItem(key, value);
} catch (error) {
console.error('Error writing to AsyncStorage:', error);
}
}
}
async removeItem(key: string): Promise<void> {
// Remove from memory storage
delete this.memoryStorage[key];
// If AsyncStorage is available, also remove from there
if (this.isAsyncStorageAvailable && typeof window !== 'undefined') {
try {
await AsyncStorage.removeItem(key);
} catch (error) {
console.error('Error removing from AsyncStorage:', error);
}
}
}
}
// Create a singleton instance
const hybridStorage = new HybridStorageService();
export default hybridStorage;

View file

@ -1,39 +0,0 @@
import { createClient } from '@supabase/supabase-js';
import { Platform } from 'react-native';
import memoryStorage from './memoryStorage';
const supabaseUrl = process.env.EXPO_PUBLIC_SUPABASE_URL;
const supabaseAnonKey = process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY;
// Überprüfen, ob die Umgebungsvariablen definiert sind
if (!supabaseUrl || !supabaseAnonKey) {
console.error('Supabase URL oder Anon Key fehlen in den Umgebungsvariablen');
}
// Web-spezifische Konfiguration
const webConfig =
Platform.OS === 'web'
? {
global: {
headers: {
'X-Client-Info': 'supabase-js-web',
},
},
// Disable realtime for web to avoid import issues
realtime: {
params: {
eventsPerSecond: 0,
},
},
}
: {};
export const supabase = createClient(supabaseUrl || '', supabaseAnonKey || '', {
auth: {
storage: memoryStorage, // Verwende benutzerdefinierte memoryStorage-Lösung
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: false,
},
...webConfig,
});