diff --git a/apps/manacore/apps/mobile/app/(drawer)/apps.tsx b/apps/manacore/apps/mobile/app/(drawer)/apps.tsx index 872c0a084..23cfb485f 100644 --- a/apps/manacore/apps/mobile/app/(drawer)/apps.tsx +++ b/apps/manacore/apps/mobile/app/(drawer)/apps.tsx @@ -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([]); - 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 - {loading ? ( - - - + {MANACORE_APPS.map((app) => ( + openAppLink(app)} + activeOpacity={0.7} > - Lade Apps... - - - ) : satellites.length === 0 ? ( - - - - Keine Apps verfügbar - - - Derzeit sind keine Apps verfügbar. Schaue später noch einmal vorbei. - - - ) : ( - - {satellites.map((satellite) => ( - handleAppClick(satellite)} - activeOpacity={0.7} - > - - {satellite.brand_logo ? ( - - console.log('Fehler beim Laden des Logos:', satellite.brand_logo) - } - /> - ) : ( - - - - )} - - - {satellite.name} - - - - {satellite.description} - + + + - openAppLink(satellite)} - disabled={ - !satellite.link_web && !satellite.link_ios && !satellite.link_android - } + - - {satellite.link_web || satellite.link_ios || satellite.link_android - ? 'Öffnen' - : 'Bald verfügbar'} - - + {app.name} + + + + {app.description} + + + + openAppLink(app)} + disabled={!app.link_web} + > + + {app.link_web ? 'Öffnen' : 'Bald verfügbar'} + - ))} - - )} + + ))} + (); 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(null); + const [members, setMembers] = useState([]); + const [loadingDetails, setLoadingDetails] = useState(true); const [deletingOrg, setDeletingOrg] = useState(false); const [showDeleteModal, setShowDeleteModal] = useState(false); - const [orgDetails, setOrgDetails] = useState(null); - const [userRole, setUserRole] = useState(''); - 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() { - {(userRole === 'org_admin' || userRole === 'system_admin') && ( - - {deletingOrg ? ( - - ) : ( - <> - - Organisation löschen - - )} - - )} + + {deletingOrg ? ( + + ) : ( + <> + + Organisation löschen + + )} + {loadingDetails ? ( @@ -418,70 +264,18 @@ export default function OrganizationDetails() { - - {isEditing ? ( - - - - {loading ? ( - - ) : ( - - )} - - - - - - ) : ( - <> - - - - {orgName} - - - - {(userRole === 'org_admin' || userRole === 'system_admin') && ( - - - - )} - - )} + + + + {orgName} + {orgDetails && ( @@ -495,20 +289,7 @@ export default function OrganizationDetails() { - {formatDate(orgDetails.created_at)} - - - - - - Teams - - - {orgDetails.team_count} + {formatDate(orgDetails.createdAt)} @@ -521,111 +302,75 @@ export default function OrganizationDetails() { - {orgDetails.user_count} + {members.length} - - - Deine Rolle - + {orgDetails.slug && ( - - {getRoleName(userRole)} + + Slug + + + {orgDetails.slug} - + )} )} + {/* Mitglieder-Liste */} - - - - Kredit-Übersicht + + Mitglieder + + + {members.length === 0 ? ( + + Keine Mitglieder gefunden. - - - {orgDetails && ( - + ) : ( + members.map((member) => ( - + + {member.name || + member.email || + `Benutzer ${member.userId.substring(0, 8)}...`} + + {member.email && member.name && ( + + {member.email} + + )} + + - Gesamt - - - {orgDetails.total_credits} - + + {getRoleName(member.role)} + + - - - - Verwendet - - - {orgDetails.used_credits} - - - - - - Verfügbar - - 0 ? (isDarkMode ? 'text-green-400' : 'text-green-600') : isDarkMode ? 'text-red-400' : 'text-red-600'}`} - > - {orgDetails.total_credits - orgDetails.used_credits} - - - + )) )} - - {/* Hier könnte später eine Liste der Teams oder Mitglieder angezeigt werden */} )} diff --git a/apps/manacore/apps/mobile/app/(drawer)/settings.tsx b/apps/manacore/apps/mobile/app/(drawer)/settings.tsx index 1ff281fd0..1a437f7d6 100644 --- a/apps/manacore/apps/mobile/app/(drawer)/settings.tsx +++ b/apps/manacore/apps/mobile/app/(drawer)/settings.tsx @@ -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(null); + const { user, signOut } = useAuth(); const [loading, setLoading] = useState(false); - const [firstName, setFirstName] = useState(''); - const [lastName, setLastName] = useState(''); - const [profile, setProfile] = useState(null); + const [name, setName] = useState(''); + const [creditBalance, setCreditBalance] = useState(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() { /> - {session && session.user ? ( + {user ? ( Mein Profil E-Mail - {session?.user?.email} + {user.email} - Vorname + Name - - Nachname - - - - {profile && profile.is_individual && ( + {creditBalance !== null && ( Verfügbare Kredite - - {profile.individual_quota - profile.individual_usage} /{' '} - {profile.individual_quota} - + {creditBalance} )} @@ -308,7 +244,7 @@ export default function SettingsScreen() { Abmelden diff --git a/apps/manacore/apps/mobile/app/(drawer)/teams/[id].tsx b/apps/manacore/apps/mobile/app/(drawer)/teams/[id].tsx index a17073746..b4a319a49 100644 --- a/apps/manacore/apps/mobile/app/(drawer)/teams/[id].tsx +++ b/apps/manacore/apps/mobile/app/(drawer)/teams/[id].tsx @@ -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 ( <> - {/* Lösch-Bestätigungsmodal */} - - - - - - - - - Team löschen - - - - {`Möchtest du das Team "${teamName}" wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.`} - - - - - - Abbrechen - - - - - {deletingTeam ? ( - - ) : ( - Löschen - )} - - - - - - @@ -268,22 +57,6 @@ export default function TeamDetails() { Zurück zu meinen Teams - - - {deletingTeam ? ( - - ) : ( - <> - - Team löschen - - )} - diff --git a/apps/manacore/apps/mobile/app/_layout.tsx b/apps/manacore/apps/mobile/app/_layout.tsx index c2b40ab8b..dbc54dbfb 100644 --- a/apps/manacore/apps/mobile/app/_layout.tsx +++ b/apps/manacore/apps/mobile/app/_layout.tsx @@ -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(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 ( - - - - - - - - + + + + + + + + + + ); diff --git a/apps/manacore/apps/mobile/app/auth/reset-password.tsx b/apps/manacore/apps/mobile/app/auth/reset-password.tsx index a334b73de..937f74b53 100644 --- a/apps/manacore/apps/mobile/app/auth/reset-password.tsx +++ b/apps/manacore/apps/mobile/app/auth/reset-password.tsx @@ -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); diff --git a/apps/manacore/apps/mobile/components/Account.tsx b/apps/manacore/apps/mobile/components/Account.tsx index 080a5e803..fd76b942d 100644 --- a/apps/manacore/apps/mobile/components/Account.tsx +++ b/apps/manacore/apps/mobile/components/Account.tsx @@ -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(null); + const [name, setName] = useState(''); + const [creditBalance, setCreditBalance] = useState(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 }) { E-Mail - {session?.user?.email} + {user?.email} - Vorname + Name - - Nachname - - - - {profile && profile.is_individual && ( + {creditBalance !== null && ( Verfügbare Kredite - - {profile.individual_quota - profile.individual_usage} / {profile.individual_quota} - + {creditBalance} )} @@ -158,7 +109,7 @@ export default function Account({ session }: { session: Session }) { Abmelden diff --git a/apps/manacore/apps/mobile/components/Auth.tsx b/apps/manacore/apps/mobile/components/Auth.tsx index e56954969..34f8a3c19 100644 --- a/apps/manacore/apps/mobile/components/Auth.tsx +++ b/apps/manacore/apps/mobile/components/Auth.tsx @@ -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 { diff --git a/apps/manacore/apps/mobile/components/CreateOrganization.tsx b/apps/manacore/apps/mobile/components/CreateOrganization.tsx index 62653ef16..f93d783e9 100644 --- a/apps/manacore/apps/mobile/components/CreateOrganization.tsx +++ b/apps/manacore/apps/mobile/components/CreateOrganization.tsx @@ -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(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 ( - - - Prüfe Berechtigungen... - - - ); - } - - if (!userHasPermission) { - return ( - - - Sie haben keine Berechtigung, Organisationen zu erstellen. Bitte kontaktieren Sie einen - Administrator. - - - ); - } - return ( @@ -231,26 +100,6 @@ export default function CreateOrganization({ onOrgCreated }: CreateOrganizationP /> - - - Anfängliche Kredite - - - - Dies ist die Gesamtanzahl der Kredite, die dieser Organisation zur Verfügung stehen - werden. - - - ); } - -// NativeWind wird für das Styling verwendet, daher sind keine StyleSheet-Definitionen erforderlich diff --git a/apps/manacore/apps/mobile/components/CreateTeam.tsx b/apps/manacore/apps/mobile/components/CreateTeam.tsx index cfd50e51d..81fd2e2dd 100644 --- a/apps/manacore/apps/mobile/components/CreateTeam.tsx +++ b/apps/manacore/apps/mobile/components/CreateTeam.tsx @@ -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(null); - const [loading, setLoading] = useState(false); - const [organizations, setOrganizations] = useState([]); - const [selectedOrgId, setSelectedOrgId] = useState(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 ( - - - Lade Organisationen... - - - ); - } - - if (organizations.length === 0) { - return ( - - - Sie haben keine Berechtigung, Teams zu erstellen, oder Sie gehören keiner Organisation an. - - - ); - } - return ( - - + + + Neues Team erstellen + + + + - Neues Team erstellen + Die Team-Erstellung wird derzeit auf das neue Auth-System migriert und ist bald wieder + verfügbar. - - - - Organisation - - - {organizations.map((org) => ( - setSelectedOrgId(org.id)} - > - - {org.name} - - - ))} - - - - - - Teamname - - - - - - - Zugewiesene Kredite - - - - - - {loading ? ( - - ) : ( - Team erstellen - )} - - + ); } - -// NativeWind wird für das Styling verwendet, daher sind keine StyleSheet-Definitionen erforderlich diff --git a/apps/manacore/apps/mobile/components/DashboardStats.tsx b/apps/manacore/apps/mobile/components/DashboardStats.tsx index 892ed8f22..475eb56d1 100644 --- a/apps/manacore/apps/mobile/components/DashboardStats.tsx +++ b/apps/manacore/apps/mobile/components/DashboardStats.tsx @@ -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(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 ( - {/* Teams und Organisationen */} + {/* Organisationen */} router.push('/teams')} - > - - - - - Teams - - - - - {teamCount} - - - - - - router.push('/organizations')} > diff --git a/apps/manacore/apps/mobile/components/OrganizationList.tsx b/apps/manacore/apps/mobile/components/OrganizationList.tsx index bde0ffb65..967bd2ce3 100644 --- a/apps/manacore/apps/mobile/components/OrganizationList.tsx +++ b/apps/manacore/apps/mobile/components/OrganizationList.tsx @@ -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; } interface OrganizationListProps { @@ -27,7 +26,7 @@ interface OrganizationListRef { const OrganizationList = forwardRef( ({ hideTitle = false }, ref) => { - const [session, setSession] = useState(null); + const { user } = useAuth(); const [loading, setLoading] = useState(true); const [organizations, setOrganizations] = useState([]); const { isDarkMode } = useTheme(); @@ -36,110 +35,32 @@ const OrganizationList = forwardRef( // 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( } } - // 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( }); }; - // 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 ( ( renderItem={({ item }) => ( { - 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( {item.name} - - - {getRoleName(item.user_role || 'member')} - - - {/* Mittlerer Bereich mit Teams und Erstellungsdatum */} - - - - - {item.team_count} {item.team_count === 1 ? 'Team' : 'Teams'} - - - - - - {formatDate(item.created_at)} - - - - - {/* Unterer Bereich mit Kreditinformationen */} - - - - Gesamte Kredite - - - {item.total_credits} - - - - - - Verwendete Kredite - - - {item.used_credits} - - - - - - Verfügbar - - 0 ? (isDarkMode ? 'text-green-400' : 'text-green-600') : isDarkMode ? 'text-red-400' : 'text-red-600'}`} - > - {item.total_credits - item.used_credits} - - + {/* Erstellungsdatum */} + + + + {formatDate(item.createdAt)} + )} @@ -384,7 +196,4 @@ const OrganizationList = forwardRef( } ); -// NativeWind wird für das Styling verwendet, daher sind keine StyleSheet-Definitionen erforderlich - -// Exportiere die Komponente mit forwardRef export default OrganizationList; diff --git a/apps/manacore/apps/mobile/components/SendMana.tsx b/apps/manacore/apps/mobile/components/SendMana.tsx index f8ca3aab3..0e1266363 100644 --- a/apps/manacore/apps/mobile/components/SendMana.tsx +++ b/apps/manacore/apps/mobile/components/SendMana.tsx @@ -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(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(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 ( Bitte melden Sie sich an, um Mana zu senden. @@ -388,79 +100,9 @@ export default function SendMana() { {userCredits} - - Empfänger E-Mail - - - - {searchLoading ? ( - - ) : ( - Suchen - )} - - - - - {foundUser && ( - - Empfänger gefunden: - {foundUser.email} - {(foundUser.first_name || foundUser.last_name) && ( - - {[foundUser.first_name, foundUser.last_name].filter(Boolean).join(' ')} - - )} - - )} - - - Mana-Betrag - - - - - Beschreibung (optional) - - - - - {loading ? ( - - ) : ( - Mana senden - )} - + + Die Mana-Übertragung zwischen Benutzern wird bald verfügbar sein. + ); } diff --git a/apps/manacore/apps/mobile/components/TeamList.tsx b/apps/manacore/apps/mobile/components/TeamList.tsx index 8cb04e4a2..e68a95eb0 100644 --- a/apps/manacore/apps/mobile/components/TeamList.tsx +++ b/apps/manacore/apps/mobile/components/TeamList.tsx @@ -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(null); - const [loading, setLoading] = useState(true); - const [teams, setTeams] = useState([]); 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 ( - - - Lade Teams... - - - ); - } - - if (teams.length === 0) { - return ( - - - - Sie sind derzeit kein Mitglied in einem Team. - - - Erstellen Sie ein neues Team oder bitten Sie einen Administrator, Sie einem Team - hinzuzufügen. - - - ); - } - return ( <> {!hideTitle && ( @@ -307,109 +40,24 @@ export default function TeamList({ hideTitle = false }: TeamListProps) { Meine Teams )} - item.id} - renderItem={({ item }) => ( - navigateToTeamDetails(item.id, item.name)} - activeOpacity={0.7} - > - {/* Header mit Teamname */} - - - - - {item.name} - - - - - - {/* Mittlerer Bereich mit Organisation und Erstellungsdatum */} - - - - - {item.organization_name} - - - - - - {formatDate(item.created_at)} - - - - - {/* Unterer Bereich mit Kreditinformationen */} - - - - Zugewiesen - - - {item.allocated_credits} - - - - - - Verwendet - - - {item.used_credits} - - - - - - Verfügbar - - 0 ? (isDarkMode ? 'text-green-400' : 'text-green-600') : isDarkMode ? 'text-red-400' : 'text-red-600'}`} - > - {(item.allocated_credits || 0) - (item.used_credits || 0)} - - - - - )} - className="px-2.5 pb-5" - /> + + + + Teams werden bald verfügbar sein. + + + Die Team-Verwaltung mit Credit-Zuweisung wird derzeit auf das neue Auth-System migriert. + + ); } - -// NativeWind wird für das Styling verwendet, daher sind keine StyleSheet-Definitionen erforderlich diff --git a/apps/manacore/apps/mobile/components/TeamMembers.tsx b/apps/manacore/apps/mobile/components/TeamMembers.tsx index e157b0aee..bad7a154c 100644 --- a/apps/manacore/apps/mobile/components/TeamMembers.tsx +++ b/apps/manacore/apps/mobile/components/TeamMembers.tsx @@ -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(null); - const [loading, setLoading] = useState(true); - const [teamDetails, setTeamDetails] = useState(null); - const [members, setMembers] = useState([]); - const [newMemberEmail, setNewMemberEmail] = useState(''); - const [inviting, setInviting] = useState(false); - const [_userRole, setUserRole] = useState(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(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 ( - - - Lade Teammitglieder... - - - ); - } - return ( - {teamDetails && ( - - {isEditing ? ( - - - - Teamname bearbeiten: - - - - - - - - - Abbrechen - - - - {updatingName ? ( - Speichern... - ) : ( - Speichern - )} - - - - Organisation: {teamDetails.organization_name} - - - Erstellt am: {formatDate(teamDetails.created_at)} - - - ) : ( - - - - {teamDetails.name} - - {isAdmin && ( - - - - )} - - - Organisation: {teamDetails.organization_name} - - - Erstellt am: {formatDate(teamDetails.created_at)} - - - )} - - )} - - {isAdmin && ( - - - Neues Mitglied einladen - - - - - {inviting ? ( - - ) : ( - Einladen - )} - - - - )} - - {members.length === 0 ? ( - - - - Keine Mitglieder in diesem Team gefunden. - - - ) : ( - item.user_id} - renderItem={({ item }) => ( - - - - {item.first_name && item.last_name - ? `${item.first_name} ${item.last_name}` - : `Benutzer ${item.user_id.substring(0, 8)}...`} - - - {editingMemberId === item.user_id ? ( - - - Credits: - - - - - - - - - - ) : ( - - isAdmin && startEditingLimit(item.user_id, item.allocated_credits) - } - className={`mr-4 flex-row items-center ${isAdmin ? 'opacity-100' : 'opacity-80'}`} - > - - Zugewiesene Credits: {item.allocated_credits} - - {isAdmin && ( - - )} - - )} - - Genutzte Credits: {item.used_credits} - - - - {isAdmin && ( - - startEditingLimit(item.user_id, item.allocated_credits)} - > - - - - removeMember( - item.user_id, - item.first_name && item.last_name - ? `${item.first_name} ${item.last_name}` - : `Benutzer ${item.user_id.substring(0, 8)}...` - ) - } - > - - - - )} - - )} - /> - )} + + + + Die Team-Mitgliederverwaltung wird derzeit auf das neue Auth-System migriert und ist + bald wieder verfügbar. + + ); diff --git a/apps/manacore/apps/mobile/context/AuthProvider.tsx b/apps/manacore/apps/mobile/context/AuthProvider.tsx new file mode 100644 index 000000000..00f2b442a --- /dev/null +++ b/apps/manacore/apps/mobile/context/AuthProvider.tsx @@ -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(key: string): Promise { + try { + const value = await SecureStore.getItemAsync(key); + return value ? JSON.parse(value) : null; + } catch { + return null; + } + }, + async setItem(key: string, value: unknown): Promise { + await SecureStore.setItemAsync(key, JSON.stringify(value)); + }, + async removeItem(key: string): Promise { + 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; + resetPassword: (email: string) => Promise<{ error: any | null }>; +}; + +// Create auth context +const AuthContext = createContext(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(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 ( + + + Authentifizierung wird initialisiert... + + ); + } + + // Provide auth context + return ( + + {children} + + ); +} diff --git a/apps/manacore/apps/mobile/package.json b/apps/manacore/apps/mobile/package.json index 80720b0b9..14913d4fe 100644 --- a/apps/manacore/apps/mobile/package.json +++ b/apps/manacore/apps/mobile/package.json @@ -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", diff --git a/apps/manacore/apps/mobile/services/api.ts b/apps/manacore/apps/mobile/services/api.ts new file mode 100644 index 000000000..3743772fe --- /dev/null +++ b/apps/manacore/apps/mobile/services/api.ts @@ -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 { + 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( + path: string, + options: RequestInit = {} + ): Promise<{ data: T | null; error: string | null }> { + try { + const token = await this.getToken(); + const headers: Record = { + 'Content-Type': 'application/json', + ...(options.headers as Record), + }; + + 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; + }> + >('/api/v1/auth/organizations'); + } + + async getOrganization(id: string) { + return this.request<{ + id: string; + name: string; + slug: string; + logo?: string; + createdAt: string; + metadata?: Record; + }>(`/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(); diff --git a/apps/manacore/apps/mobile/utils/memoryStorage.ts b/apps/manacore/apps/mobile/utils/memoryStorage.ts deleted file mode 100644 index 6a3cbd00f..000000000 --- a/apps/manacore/apps/mobile/utils/memoryStorage.ts +++ /dev/null @@ -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 { - 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 { - 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 { - // 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 { - // 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 { - // 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; diff --git a/apps/manacore/apps/mobile/utils/supabase.ts b/apps/manacore/apps/mobile/utils/supabase.ts deleted file mode 100644 index c04dc5889..000000000 --- a/apps/manacore/apps/mobile/utils/supabase.ts +++ /dev/null @@ -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, -});