mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-17 07:39:39 +02:00
refactor(manacore-mobile): migrate from Supabase to mana-core-auth
Complete migration of ManaCore mobile app from direct Supabase access
to mana-core-auth API. Removes @supabase/supabase-js dependency entirely.
New files:
- context/AuthProvider.tsx: mana-core-auth integration via @manacore/shared-auth
- services/api.ts: ManaCoreApi client wrapping mana-core-auth REST endpoints
Migrated (17 files):
- Auth: signIn/signUp/signOut → @manacore/shared-auth with SecureStore
- Profiles: supabase.from('profiles') → /api/v1/auth/profile
- Organizations: supabase.from('organizations') → /api/v1/auth/organizations
- Credits: supabase.from('credit_transactions') → /api/v1/credits/balance
- Members: supabase.from('user_roles') → /api/v1/auth/organizations/:id/members
Simplified (teams not yet in mana-core-auth):
- CreateTeam, TeamList, TeamMembers → placeholder with TODO
- SendMana → shows balance only, transfer coming later
Deleted:
- utils/supabase.ts, utils/memoryStorage.ts
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
99d16673bd
commit
343f30e321
20 changed files with 914 additions and 3487 deletions
|
|
@ -1,110 +1,75 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { Stack } from 'expo-router';
|
||||
import {
|
||||
ScrollView,
|
||||
Text,
|
||||
View,
|
||||
Image,
|
||||
TouchableOpacity,
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
Platform,
|
||||
Linking,
|
||||
} from 'react-native';
|
||||
import { ScrollView, Text, View, TouchableOpacity, Platform, Linking, Alert } from 'react-native';
|
||||
import { FontAwesome5 } from '@expo/vector-icons';
|
||||
|
||||
import { Container } from '~/components/Container';
|
||||
import { useTheme } from '../../utils/themeContext';
|
||||
import { supabase } from '../../utils/supabase';
|
||||
|
||||
// Interface für die Satellite-Daten
|
||||
interface Satellite {
|
||||
// TODO: App-Liste (Satellites) war zuvor in Supabase gespeichert.
|
||||
// Bis diese Daten über mana-core-auth oder einen eigenen Endpunkt verfügbar sind,
|
||||
// verwenden wir eine statische Liste der bekannten ManaCore-Apps.
|
||||
|
||||
interface AppInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
brand_logo: string;
|
||||
created_at: string;
|
||||
icon: string;
|
||||
link_web?: string;
|
||||
link_ios?: string;
|
||||
link_android?: string;
|
||||
}
|
||||
|
||||
const MANACORE_APPS: AppInfo[] = [
|
||||
{
|
||||
id: 'chat',
|
||||
name: 'Chat',
|
||||
description: 'KI-Chat-Anwendung mit verschiedenen Modellen',
|
||||
icon: 'comments',
|
||||
link_web: 'https://chat.mana.how',
|
||||
},
|
||||
{
|
||||
id: 'picture',
|
||||
name: 'Picture',
|
||||
description: 'KI-Bildgenerierung',
|
||||
icon: 'image',
|
||||
link_web: 'https://picture.mana.how',
|
||||
},
|
||||
{
|
||||
id: 'zitare',
|
||||
name: 'Zitare',
|
||||
description: 'Tägliche Inspirationszitate',
|
||||
icon: 'quote-left',
|
||||
link_web: 'https://zitare.mana.how',
|
||||
},
|
||||
{
|
||||
id: 'manadeck',
|
||||
name: 'ManaDeck',
|
||||
description: 'Karten- und Deck-Verwaltung',
|
||||
icon: 'layer-group',
|
||||
link_web: 'https://manadeck.mana.how',
|
||||
},
|
||||
{
|
||||
id: 'contacts',
|
||||
name: 'Contacts',
|
||||
description: 'Kontaktverwaltung',
|
||||
icon: 'address-book',
|
||||
},
|
||||
];
|
||||
|
||||
export default function Apps() {
|
||||
const { isDarkMode } = useTheme();
|
||||
const [satellites, setSatellites] = useState<Satellite[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetchSatellites();
|
||||
}, []);
|
||||
|
||||
const fetchSatellites = async () => {
|
||||
const openAppLink = async (app: AppInfo) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// Hole die Satellites-Daten aus der Supabase-Datenbank
|
||||
const { data, error } = await supabase.from('satellites').select('*').order('name');
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (data) {
|
||||
console.log('Satellites geladen:', data.length);
|
||||
setSatellites(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Satellites:', error);
|
||||
Alert.alert(
|
||||
'Fehler',
|
||||
'Beim Laden der Apps ist ein Fehler aufgetreten. Bitte versuche es später erneut.'
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAppClick = (satellite: Satellite) => {
|
||||
// Öffne die App-Details in einem Alert
|
||||
console.log('App angeklickt:', satellite.name);
|
||||
Alert.alert(satellite.name, satellite.description, [{ text: 'OK' }]);
|
||||
};
|
||||
|
||||
const openAppLink = async (satellite: Satellite) => {
|
||||
try {
|
||||
let linkToOpen: string | undefined;
|
||||
|
||||
// Wähle den entsprechenden Link basierend auf der Plattform
|
||||
if (Platform.OS === 'ios') {
|
||||
linkToOpen = satellite.link_ios;
|
||||
} else if (Platform.OS === 'android') {
|
||||
linkToOpen = satellite.link_android;
|
||||
} else {
|
||||
// Für Web oder wenn die plattformspezifischen Links nicht verfügbar sind
|
||||
linkToOpen = satellite.link_web;
|
||||
}
|
||||
|
||||
// Fallback: Wenn kein plattformspezifischer Link vorhanden ist, versuche den Web-Link
|
||||
if (!linkToOpen) {
|
||||
linkToOpen = satellite.link_web;
|
||||
}
|
||||
|
||||
// Wenn kein Link verfügbar ist, zeige eine Meldung an
|
||||
if (!linkToOpen) {
|
||||
Alert.alert('Bald verfügbar', `${satellite.name} wird bald verfügbar sein.`, [
|
||||
{ text: 'OK' },
|
||||
]);
|
||||
if (!app.link_web) {
|
||||
Alert.alert('Bald verfügbar', `${app.name} wird bald verfügbar sein.`, [{ text: 'OK' }]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Prüfe, ob der Link geöffnet werden kann
|
||||
const canOpen = await Linking.canOpenURL(linkToOpen);
|
||||
|
||||
const canOpen = await Linking.canOpenURL(app.link_web);
|
||||
if (canOpen) {
|
||||
// Öffne den Link
|
||||
await Linking.openURL(linkToOpen);
|
||||
await Linking.openURL(app.link_web);
|
||||
} else {
|
||||
Alert.alert('Fehler', `Der Link für ${satellite.name} kann nicht geöffnet werden.`, [
|
||||
Alert.alert('Fehler', `Der Link für ${app.name} kann nicht geöffnet werden.`, [
|
||||
{ text: 'OK' },
|
||||
]);
|
||||
}
|
||||
|
|
@ -134,95 +99,50 @@ export default function Apps() {
|
|||
Entdecke Apps, die mit Manacore verbunden werden können
|
||||
</Text>
|
||||
|
||||
{loading ? (
|
||||
<View
|
||||
className={`flex-1 items-center justify-center p-10 ${isDarkMode ? 'bg-gray-800' : 'bg-white'} rounded-lg shadow`}
|
||||
>
|
||||
<ActivityIndicator size="large" color={isDarkMode ? '#93C5FD' : '#0055FF'} />
|
||||
<Text
|
||||
className={`mt-4 text-base ${isDarkMode ? 'text-gray-300' : 'text-gray-600'}`}
|
||||
<View className="flex-row flex-wrap justify-between">
|
||||
{MANACORE_APPS.map((app) => (
|
||||
<TouchableOpacity
|
||||
key={app.id}
|
||||
className={`mb-4 w-[48%] overflow-hidden rounded-xl shadow ${isDarkMode ? 'bg-gray-800' : 'bg-white'}`}
|
||||
onPress={() => openAppLink(app)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
Lade Apps...
|
||||
</Text>
|
||||
</View>
|
||||
) : satellites.length === 0 ? (
|
||||
<View
|
||||
className={`p-8 ${isDarkMode ? 'bg-gray-800' : 'bg-white'} items-center rounded-lg shadow`}
|
||||
>
|
||||
<FontAwesome5 name="rocket" size={50} color={isDarkMode ? '#4B5563' : '#9CA3AF'} />
|
||||
<Text
|
||||
className={`mt-4 text-center text-lg font-medium ${isDarkMode ? 'text-gray-300' : 'text-gray-600'}`}
|
||||
>
|
||||
Keine Apps verfügbar
|
||||
</Text>
|
||||
<Text
|
||||
className={`mt-2 text-center text-sm ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`}
|
||||
>
|
||||
Derzeit sind keine Apps verfügbar. Schaue später noch einmal vorbei.
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
<View className="flex-row flex-wrap justify-between">
|
||||
{satellites.map((satellite) => (
|
||||
<TouchableOpacity
|
||||
key={satellite.id}
|
||||
className={`mb-4 w-[48%] overflow-hidden rounded-xl shadow ${isDarkMode ? 'bg-gray-800' : 'bg-white'}`}
|
||||
onPress={() => handleAppClick(satellite)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View className="items-center p-4">
|
||||
{satellite.brand_logo ? (
|
||||
<Image
|
||||
source={{ uri: satellite.brand_logo }}
|
||||
className="mb-3 h-16 w-16 rounded-lg"
|
||||
resizeMode="contain"
|
||||
// Fallback für den Fall, dass das Bild nicht geladen werden kann
|
||||
onError={() =>
|
||||
console.log('Fehler beim Laden des Logos:', satellite.brand_logo)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<View className="mb-3 h-16 w-16 items-center justify-center rounded-lg bg-gray-200">
|
||||
<FontAwesome5
|
||||
name="rocket"
|
||||
size={24}
|
||||
color={isDarkMode ? '#4B5563' : '#9CA3AF'}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<Text
|
||||
className={`mb-1 text-center text-base font-bold ${isDarkMode ? 'text-gray-100' : 'text-gray-800'}`}
|
||||
>
|
||||
{satellite.name}
|
||||
</Text>
|
||||
|
||||
<Text
|
||||
className={`text-center text-xs ${isDarkMode ? 'text-gray-300' : 'text-gray-600'}`}
|
||||
numberOfLines={2}
|
||||
ellipsizeMode="tail"
|
||||
>
|
||||
{satellite.description}
|
||||
</Text>
|
||||
<View className="items-center p-4">
|
||||
<View className="mb-3 h-16 w-16 items-center justify-center rounded-lg bg-gray-200">
|
||||
<FontAwesome5
|
||||
name={app.icon}
|
||||
size={24}
|
||||
color={isDarkMode ? '#60A5FA' : '#0055FF'}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
className={`w-full py-2 ${isDarkMode ? 'bg-blue-700' : 'bg-blue-600'}`}
|
||||
onPress={() => openAppLink(satellite)}
|
||||
disabled={
|
||||
!satellite.link_web && !satellite.link_ios && !satellite.link_android
|
||||
}
|
||||
<Text
|
||||
className={`mb-1 text-center text-base font-bold ${isDarkMode ? 'text-gray-100' : 'text-gray-800'}`}
|
||||
>
|
||||
<Text className="text-center text-sm font-medium text-white">
|
||||
{satellite.link_web || satellite.link_ios || satellite.link_android
|
||||
? 'Öffnen'
|
||||
: 'Bald verfügbar'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
{app.name}
|
||||
</Text>
|
||||
|
||||
<Text
|
||||
className={`text-center text-xs ${isDarkMode ? 'text-gray-300' : 'text-gray-600'}`}
|
||||
numberOfLines={2}
|
||||
ellipsizeMode="tail"
|
||||
>
|
||||
{app.description}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
className={`w-full py-2 ${isDarkMode ? 'bg-blue-700' : 'bg-blue-600'}`}
|
||||
onPress={() => openAppLink(app)}
|
||||
disabled={!app.link_web}
|
||||
>
|
||||
<Text className="text-center text-sm font-medium text-white">
|
||||
{app.link_web ? 'Öffnen' : 'Bald verfügbar'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
|
||||
<View className={`mt-4 rounded-lg p-4 ${isDarkMode ? 'bg-gray-800' : 'bg-gray-100'}`}>
|
||||
<Text
|
||||
|
|
|
|||
|
|
@ -9,43 +9,40 @@ import {
|
|||
Alert,
|
||||
ActivityIndicator,
|
||||
Modal,
|
||||
FlatList,
|
||||
} from 'react-native';
|
||||
import { FontAwesome5 } from '@expo/vector-icons';
|
||||
|
||||
import { Container } from '~/components/Container';
|
||||
import { useTheme } from '../../../utils/themeContext';
|
||||
import { supabase } from '../../../utils/supabase';
|
||||
import { useAuth } from '../../../context/AuthProvider';
|
||||
import { api } from '../../../services/api';
|
||||
|
||||
interface OrganizationDetails {
|
||||
interface OrgMember {
|
||||
id: string;
|
||||
name: string;
|
||||
total_credits: number;
|
||||
used_credits: number;
|
||||
created_at: string;
|
||||
team_count: number;
|
||||
user_count: number;
|
||||
userId: string;
|
||||
email?: string;
|
||||
name?: string;
|
||||
role: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export default function OrganizationDetails() {
|
||||
const router = useRouter();
|
||||
const { id: orgId, name: initialOrgName } = useLocalSearchParams<{ id: string; name: string }>();
|
||||
const { isDarkMode } = useTheme();
|
||||
const { user } = useAuth();
|
||||
const [orgName, setOrgName] = useState(initialOrgName || '');
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [newOrgName, setNewOrgName] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [orgDetails, setOrgDetails] = useState<any>(null);
|
||||
const [members, setMembers] = useState<OrgMember[]>([]);
|
||||
const [loadingDetails, setLoadingDetails] = useState(true);
|
||||
const [deletingOrg, setDeletingOrg] = useState(false);
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
const [orgDetails, setOrgDetails] = useState<OrganizationDetails | null>(null);
|
||||
const [userRole, setUserRole] = useState<string>('');
|
||||
const [loadingDetails, setLoadingDetails] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialOrgName) {
|
||||
setOrgName(initialOrgName);
|
||||
setNewOrgName(initialOrgName);
|
||||
}
|
||||
|
||||
fetchOrganizationDetails();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [initialOrgName, orgId]);
|
||||
|
|
@ -56,84 +53,20 @@ export default function OrganizationDetails() {
|
|||
try {
|
||||
setLoadingDetails(true);
|
||||
|
||||
// Hole die Organisation
|
||||
const { data: org, error: orgError } = await supabase
|
||||
.from('organizations')
|
||||
.select('id, name, total_credits, used_credits, created_at')
|
||||
.eq('id', orgId)
|
||||
.single();
|
||||
const { data: org, error: orgError } = await api.getOrganization(orgId);
|
||||
if (orgError) throw new Error(orgError);
|
||||
|
||||
if (orgError) throw orgError;
|
||||
|
||||
// Hole die Anzahl der Teams in dieser Organisation
|
||||
const { count: teamCount, error: teamCountError } = await supabase
|
||||
.from('teams')
|
||||
.select('id', { count: 'exact', head: true })
|
||||
.eq('organization_id', orgId);
|
||||
|
||||
if (teamCountError) throw teamCountError;
|
||||
|
||||
// Hole die Anzahl der Benutzer in dieser Organisation
|
||||
const { data: userRoles, error: userRolesError } = await supabase
|
||||
.from('user_roles')
|
||||
.select('user_id')
|
||||
.eq('organization_id', orgId);
|
||||
|
||||
if (userRolesError) throw userRolesError;
|
||||
|
||||
// Entferne Duplikate (ein Benutzer kann mehrere Rollen haben)
|
||||
const uniqueUserIds = [...new Set(userRoles.map((role) => role.user_id))];
|
||||
|
||||
// Hole die aktuelle Benutzerrolle
|
||||
const {
|
||||
data: { session },
|
||||
} = await supabase.auth.getSession();
|
||||
if (session?.user) {
|
||||
const { data: currentUserRoles, error: currentUserRolesError } = await supabase
|
||||
.from('user_roles')
|
||||
.select('roles(name)')
|
||||
.eq('user_id', session.user.id)
|
||||
.eq('organization_id', orgId);
|
||||
|
||||
if (currentUserRolesError) throw currentUserRolesError;
|
||||
|
||||
// Finde die höchste Rolle
|
||||
const roleHierarchy = {
|
||||
system_admin: 4,
|
||||
org_admin: 3,
|
||||
team_admin: 2,
|
||||
member: 1,
|
||||
};
|
||||
|
||||
let highestRole = 'member';
|
||||
let highestRoleValue = 0;
|
||||
|
||||
// Typensichere Iteration über die Benutzerrollen
|
||||
// Wir müssen die Daten zuerst in das erwartete Format konvertieren
|
||||
currentUserRoles.forEach((userRole: any) => {
|
||||
// Prüfe, ob die Rolle existiert und ein Name vorhanden ist
|
||||
const roleName = userRole.roles?.name;
|
||||
if (
|
||||
roleName &&
|
||||
roleHierarchy[roleName as keyof typeof roleHierarchy] > highestRoleValue
|
||||
) {
|
||||
highestRole = roleName;
|
||||
highestRoleValue = roleHierarchy[roleName as keyof typeof roleHierarchy];
|
||||
}
|
||||
});
|
||||
|
||||
setUserRole(highestRole);
|
||||
if (org) {
|
||||
setOrgDetails(org);
|
||||
setOrgName(org.name);
|
||||
}
|
||||
|
||||
setOrgDetails({
|
||||
...org,
|
||||
team_count: teamCount || 0,
|
||||
user_count: uniqueUserIds.length,
|
||||
});
|
||||
|
||||
setOrgName(org.name);
|
||||
setNewOrgName(org.name);
|
||||
} catch (error) {
|
||||
// Fetch members
|
||||
const { data: membersData } = await api.getOrgMembers(orgId);
|
||||
if (membersData) {
|
||||
setMembers(membersData);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Fehler beim Laden der Organisationsdetails:', error);
|
||||
Alert.alert('Fehler', 'Es ist ein Fehler beim Laden der Organisationsdetails aufgetreten.');
|
||||
} finally {
|
||||
|
|
@ -145,60 +78,16 @@ export default function OrganizationDetails() {
|
|||
router.replace('/organizations');
|
||||
};
|
||||
|
||||
const toggleEditMode = () => {
|
||||
if (isEditing) {
|
||||
setIsEditing(false);
|
||||
} else {
|
||||
setNewOrgName(orgName);
|
||||
setIsEditing(true);
|
||||
}
|
||||
};
|
||||
|
||||
const updateOrganizationName = async () => {
|
||||
if (!orgId || !newOrgName.trim() || newOrgName.trim() === orgName) {
|
||||
setIsEditing(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const { error } = await supabase
|
||||
.from('organizations')
|
||||
.update({ name: newOrgName.trim() })
|
||||
.eq('id', orgId);
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
setOrgName(newOrgName.trim());
|
||||
setIsEditing(false);
|
||||
|
||||
Alert.alert('Erfolg', 'Der Organisationsname wurde erfolgreich aktualisiert.');
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Aktualisieren des Organisationsnamens:', error);
|
||||
Alert.alert(
|
||||
'Fehler',
|
||||
'Es ist ein Fehler beim Aktualisieren des Organisationsnamens aufgetreten.'
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteOrg = () => {
|
||||
console.log('Delete organization button clicked, orgId:', orgId);
|
||||
// Modal öffnen statt Alert anzeigen
|
||||
setShowDeleteModal(true);
|
||||
};
|
||||
|
||||
const confirmDelete = () => {
|
||||
console.log('Löschen bestätigt für Organisation:', orgId);
|
||||
setShowDeleteModal(false);
|
||||
handleOrgDeletion();
|
||||
};
|
||||
|
||||
const cancelDelete = () => {
|
||||
console.log('Löschen abgebrochen');
|
||||
setShowDeleteModal(false);
|
||||
};
|
||||
|
||||
|
|
@ -208,65 +97,24 @@ export default function OrganizationDetails() {
|
|||
try {
|
||||
setDeletingOrg(true);
|
||||
|
||||
console.log('Starte Löschvorgang für Organisation:', orgId);
|
||||
const { error } = await api.deleteOrganization(orgId);
|
||||
|
||||
// 1. Prüfe, ob es Teams in dieser Organisation gibt
|
||||
const { count: teamCount, error: teamCountError } = await supabase
|
||||
.from('teams')
|
||||
.select('id', { count: 'exact', head: true })
|
||||
.eq('organization_id', orgId);
|
||||
if (error) throw new Error(error);
|
||||
|
||||
if (teamCountError) throw teamCountError;
|
||||
|
||||
if (teamCount && teamCount > 0) {
|
||||
Alert.alert(
|
||||
'Fehler',
|
||||
'Diese Organisation enthält noch Teams. Bitte lösche zuerst alle Teams, bevor du die Organisation löschst.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Lösche alle Benutzerrollen für diese Organisation
|
||||
console.log('Lösche Benutzerrollen für Organisation:', orgId);
|
||||
const { error: userRolesError } = await supabase
|
||||
.from('user_roles')
|
||||
.delete()
|
||||
.eq('organization_id', orgId);
|
||||
|
||||
if (userRolesError) throw userRolesError;
|
||||
|
||||
// 3. Lösche die Organisation
|
||||
console.log('Lösche Organisation:', orgId);
|
||||
const { error: orgError } = await supabase.from('organizations').delete().eq('id', orgId);
|
||||
|
||||
if (orgError) throw orgError;
|
||||
|
||||
console.log('Organisation erfolgreich gelöscht');
|
||||
|
||||
// Erfolgsmeldung anzeigen und zurück zur Organisationsliste navigieren
|
||||
Alert.alert('Erfolg', 'Die Organisation wurde erfolgreich gelöscht.', [
|
||||
{
|
||||
text: 'OK',
|
||||
onPress: () => {
|
||||
console.log('Navigating back to organizations list');
|
||||
// Zurück zur Organisationsliste navigieren
|
||||
router.replace('/organizations');
|
||||
},
|
||||
onPress: () => router.replace('/organizations'),
|
||||
},
|
||||
]);
|
||||
|
||||
// Auch ohne Klick auf OK zurück zur Organisationsliste navigieren (nach kurzer Verzögerung)
|
||||
setTimeout(() => {
|
||||
router.replace('/organizations');
|
||||
}, 1000);
|
||||
} catch (error: any) {
|
||||
console.error('Fehler beim Löschen der Organisation:', error);
|
||||
|
||||
// Detaillierte Fehlermeldung anzeigen
|
||||
Alert.alert(
|
||||
'Fehler',
|
||||
`Es ist ein Fehler beim Löschen der Organisation aufgetreten: ${error?.message || JSON.stringify(error)}`,
|
||||
[{ text: 'OK' }]
|
||||
`Es ist ein Fehler beim Löschen der Organisation aufgetreten: ${error?.message || 'Unbekannter Fehler'}`
|
||||
);
|
||||
} finally {
|
||||
setDeletingOrg(false);
|
||||
|
|
@ -284,14 +132,14 @@ export default function OrganizationDetails() {
|
|||
|
||||
const getRoleName = (role: string) => {
|
||||
switch (role) {
|
||||
case 'system_admin':
|
||||
return 'System-Admin';
|
||||
case 'org_admin':
|
||||
return 'Organisations-Admin';
|
||||
case 'team_admin':
|
||||
return 'Team-Admin';
|
||||
default:
|
||||
case 'owner':
|
||||
return 'Eigentümer';
|
||||
case 'admin':
|
||||
return 'Administrator';
|
||||
case 'member':
|
||||
return 'Mitglied';
|
||||
default:
|
||||
return role;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -383,23 +231,21 @@ export default function OrganizationDetails() {
|
|||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
{(userRole === 'org_admin' || userRole === 'system_admin') && (
|
||||
<TouchableOpacity
|
||||
className={`flex-row items-center rounded-lg px-4 py-2 ${deletingOrg ? 'bg-gray-500' : 'bg-red-600'}`}
|
||||
onPress={deleteOrg}
|
||||
disabled={deletingOrg}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
{deletingOrg ? (
|
||||
<ActivityIndicator size="small" color="white" />
|
||||
) : (
|
||||
<>
|
||||
<FontAwesome5 name="trash-alt" size={16} color="white" className="mr-2" />
|
||||
<Text className="text-sm font-semibold text-white">Organisation löschen</Text>
|
||||
</>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<TouchableOpacity
|
||||
className={`flex-row items-center rounded-lg px-4 py-2 ${deletingOrg ? 'bg-gray-500' : 'bg-red-600'}`}
|
||||
onPress={deleteOrg}
|
||||
disabled={deletingOrg}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
{deletingOrg ? (
|
||||
<ActivityIndicator size="small" color="white" />
|
||||
) : (
|
||||
<>
|
||||
<FontAwesome5 name="trash-alt" size={16} color="white" className="mr-2" />
|
||||
<Text className="text-sm font-semibold text-white">Organisation löschen</Text>
|
||||
</>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{loadingDetails ? (
|
||||
|
|
@ -418,70 +264,18 @@ export default function OrganizationDetails() {
|
|||
<View
|
||||
className={`${isDarkMode ? 'bg-gray-800' : 'bg-white'} m-2.5 rounded-lg p-5 shadow`}
|
||||
>
|
||||
<View className="mb-4 flex-row items-center justify-between">
|
||||
{isEditing ? (
|
||||
<View className="flex-1 flex-row items-center">
|
||||
<TextInput
|
||||
className={`h-12 flex-1 border ${isDarkMode ? 'border-gray-600 bg-gray-700 text-white' : 'border-gray-300 bg-gray-50 text-gray-800'} rounded-lg px-4 text-base`}
|
||||
value={newOrgName}
|
||||
onChangeText={setNewOrgName}
|
||||
placeholder="Organisationsname"
|
||||
placeholderTextColor={isDarkMode ? '#9CA3AF' : '#9CA3AF'}
|
||||
autoFocus
|
||||
/>
|
||||
<TouchableOpacity
|
||||
className="ml-2 p-2"
|
||||
onPress={updateOrganizationName}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator
|
||||
size="small"
|
||||
color={isDarkMode ? '#93C5FD' : '#0055FF'}
|
||||
/>
|
||||
) : (
|
||||
<FontAwesome5
|
||||
name="check"
|
||||
size={20}
|
||||
color={isDarkMode ? '#93C5FD' : '#0055FF'}
|
||||
/>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity className="ml-2 p-2" onPress={toggleEditMode}>
|
||||
<FontAwesome5
|
||||
name="times"
|
||||
size={20}
|
||||
color={isDarkMode ? '#F87171' : '#EF4444'}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
<View className="flex-row items-center">
|
||||
<FontAwesome5
|
||||
name="building"
|
||||
size={24}
|
||||
color={isDarkMode ? '#93C5FD' : '#0055FF'}
|
||||
className="mr-3"
|
||||
/>
|
||||
<Text
|
||||
className={`text-2xl font-bold ${isDarkMode ? 'text-gray-100' : 'text-gray-800'}`}
|
||||
>
|
||||
{orgName}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{(userRole === 'org_admin' || userRole === 'system_admin') && (
|
||||
<TouchableOpacity className="p-2" onPress={toggleEditMode}>
|
||||
<FontAwesome5
|
||||
name="edit"
|
||||
size={18}
|
||||
color={isDarkMode ? '#93C5FD' : '#0055FF'}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<View className="mb-4 flex-row items-center">
|
||||
<FontAwesome5
|
||||
name="building"
|
||||
size={24}
|
||||
color={isDarkMode ? '#93C5FD' : '#0055FF'}
|
||||
className="mr-3"
|
||||
/>
|
||||
<Text
|
||||
className={`text-2xl font-bold ${isDarkMode ? 'text-gray-100' : 'text-gray-800'}`}
|
||||
>
|
||||
{orgName}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{orgDetails && (
|
||||
|
|
@ -495,20 +289,7 @@ export default function OrganizationDetails() {
|
|||
<Text
|
||||
className={`font-medium ${isDarkMode ? 'text-gray-100' : 'text-gray-800'}`}
|
||||
>
|
||||
{formatDate(orgDetails.created_at)}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View
|
||||
className={`flex-row justify-between border-t py-3 ${isDarkMode ? 'border-gray-700' : 'border-gray-200'}`}
|
||||
>
|
||||
<Text className={`${isDarkMode ? 'text-gray-300' : 'text-gray-600'}`}>
|
||||
Teams
|
||||
</Text>
|
||||
<Text
|
||||
className={`font-medium ${isDarkMode ? 'text-gray-100' : 'text-gray-800'}`}
|
||||
>
|
||||
{orgDetails.team_count}
|
||||
{formatDate(orgDetails.createdAt)}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
|
|
@ -521,111 +302,75 @@ export default function OrganizationDetails() {
|
|||
<Text
|
||||
className={`font-medium ${isDarkMode ? 'text-gray-100' : 'text-gray-800'}`}
|
||||
>
|
||||
{orgDetails.user_count}
|
||||
{members.length}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View
|
||||
className={`flex-row justify-between border-t py-3 ${isDarkMode ? 'border-gray-700' : 'border-gray-200'}`}
|
||||
>
|
||||
<Text className={`${isDarkMode ? 'text-gray-300' : 'text-gray-600'}`}>
|
||||
Deine Rolle
|
||||
</Text>
|
||||
{orgDetails.slug && (
|
||||
<View
|
||||
className={`rounded-full px-2 py-0.5 ${userRole === 'system_admin' ? 'bg-red-600' : userRole === 'org_admin' ? 'bg-orange-500' : userRole === 'team_admin' ? 'bg-green-600' : 'bg-gray-600'}`}
|
||||
className={`flex-row justify-between border-t py-3 ${isDarkMode ? 'border-gray-700' : 'border-gray-200'}`}
|
||||
>
|
||||
<Text className="text-xs font-medium text-white">
|
||||
{getRoleName(userRole)}
|
||||
<Text className={`${isDarkMode ? 'text-gray-300' : 'text-gray-600'}`}>
|
||||
Slug
|
||||
</Text>
|
||||
<Text
|
||||
className={`font-medium ${isDarkMode ? 'text-gray-100' : 'text-gray-800'}`}
|
||||
>
|
||||
{orgDetails.slug}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Mitglieder-Liste */}
|
||||
<View
|
||||
className={`${isDarkMode ? 'bg-gray-800' : 'bg-white'} m-2.5 rounded-lg p-5 shadow`}
|
||||
>
|
||||
<View className="mb-4 flex-row items-center">
|
||||
<FontAwesome5
|
||||
name="coins"
|
||||
size={20}
|
||||
color={isDarkMode ? '#93C5FD' : '#0055FF'}
|
||||
className="mr-2"
|
||||
/>
|
||||
<Text
|
||||
className={`text-xl font-bold ${isDarkMode ? 'text-gray-100' : 'text-gray-800'}`}
|
||||
>
|
||||
Kredit-Übersicht
|
||||
<Text
|
||||
className={`mb-4 text-xl font-bold ${isDarkMode ? 'text-gray-100' : 'text-gray-800'}`}
|
||||
>
|
||||
Mitglieder
|
||||
</Text>
|
||||
|
||||
{members.length === 0 ? (
|
||||
<Text className={`text-center ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`}>
|
||||
Keine Mitglieder gefunden.
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{orgDetails && (
|
||||
<View className="flex-row justify-between">
|
||||
) : (
|
||||
members.map((member) => (
|
||||
<View
|
||||
className="mr-2 flex-1 items-center rounded-lg bg-opacity-20 p-3"
|
||||
style={{
|
||||
backgroundColor: isDarkMode
|
||||
? 'rgba(147, 197, 253, 0.1)'
|
||||
: 'rgba(0, 85, 255, 0.1)',
|
||||
}}
|
||||
key={member.id}
|
||||
className={`mb-2 flex-row items-center justify-between rounded-lg p-3 ${isDarkMode ? 'bg-gray-700' : 'bg-gray-50'}`}
|
||||
>
|
||||
<Text
|
||||
className={`text-sm ${isDarkMode ? 'text-gray-300' : 'text-gray-600'} mb-1`}
|
||||
<View>
|
||||
<Text
|
||||
className={`text-base font-medium ${isDarkMode ? 'text-gray-100' : 'text-gray-800'}`}
|
||||
>
|
||||
{member.name ||
|
||||
member.email ||
|
||||
`Benutzer ${member.userId.substring(0, 8)}...`}
|
||||
</Text>
|
||||
{member.email && member.name && (
|
||||
<Text
|
||||
className={`text-sm ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`}
|
||||
>
|
||||
{member.email}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
<View
|
||||
className={`rounded-full px-2 py-0.5 ${member.role === 'owner' ? 'bg-red-600' : member.role === 'admin' ? 'bg-orange-500' : 'bg-gray-600'}`}
|
||||
>
|
||||
Gesamt
|
||||
</Text>
|
||||
<Text
|
||||
className={`text-2xl font-bold ${isDarkMode ? 'text-gray-100' : 'text-gray-800'}`}
|
||||
>
|
||||
{orgDetails.total_credits}
|
||||
</Text>
|
||||
<Text className="text-xs font-medium text-white">
|
||||
{getRoleName(member.role)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View
|
||||
className="mx-1 flex-1 items-center rounded-lg bg-opacity-20 p-3"
|
||||
style={{
|
||||
backgroundColor: isDarkMode
|
||||
? 'rgba(147, 197, 253, 0.1)'
|
||||
: 'rgba(0, 85, 255, 0.1)',
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
className={`text-sm ${isDarkMode ? 'text-gray-300' : 'text-gray-600'} mb-1`}
|
||||
>
|
||||
Verwendet
|
||||
</Text>
|
||||
<Text
|
||||
className={`text-2xl font-bold ${isDarkMode ? 'text-gray-100' : 'text-gray-800'}`}
|
||||
>
|
||||
{orgDetails.used_credits}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View
|
||||
className="ml-2 flex-1 items-center rounded-lg bg-opacity-20 p-3"
|
||||
style={{
|
||||
backgroundColor: isDarkMode
|
||||
? 'rgba(147, 197, 253, 0.1)'
|
||||
: 'rgba(0, 85, 255, 0.1)',
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
className={`text-sm ${isDarkMode ? 'text-gray-300' : 'text-gray-600'} mb-1`}
|
||||
>
|
||||
Verfügbar
|
||||
</Text>
|
||||
<Text
|
||||
className={`text-2xl font-bold ${orgDetails.total_credits - orgDetails.used_credits > 0 ? (isDarkMode ? 'text-green-400' : 'text-green-600') : isDarkMode ? 'text-red-400' : 'text-red-600'}`}
|
||||
>
|
||||
{orgDetails.total_credits - orgDetails.used_credits}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Hier könnte später eine Liste der Teams oder Mitglieder angezeigt werden */}
|
||||
</>
|
||||
)}
|
||||
</ScrollView>
|
||||
|
|
|
|||
|
|
@ -13,26 +13,15 @@ import { FontAwesome5 } from '@expo/vector-icons';
|
|||
import { Container } from '~/components/Container';
|
||||
import { useTheme } from '~/utils/themeContext';
|
||||
import type { ThemeMode } from '~/utils/themeContext';
|
||||
import { supabase } from '../../utils/supabase';
|
||||
import { Session } from '@supabase/supabase-js';
|
||||
|
||||
interface Profile {
|
||||
id: string;
|
||||
first_name: string | null;
|
||||
last_name: string | null;
|
||||
avatar_url: string | null;
|
||||
is_individual: boolean;
|
||||
individual_quota: number;
|
||||
individual_usage: number;
|
||||
}
|
||||
import { useAuth } from '~/context/AuthProvider';
|
||||
import { api } from '~/services/api';
|
||||
|
||||
export default function SettingsScreen() {
|
||||
const { themeMode, setThemeMode, isDarkMode } = useTheme();
|
||||
const [session, setSession] = useState<Session | null>(null);
|
||||
const { user, signOut } = useAuth();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [firstName, setFirstName] = useState('');
|
||||
const [lastName, setLastName] = useState('');
|
||||
const [profile, setProfile] = useState<Profile | null>(null);
|
||||
const [name, setName] = useState('');
|
||||
const [creditBalance, setCreditBalance] = useState<number | null>(null);
|
||||
|
||||
// Funktion zum Ändern des Theme-Modus
|
||||
const changeTheme = (mode: ThemeMode) => {
|
||||
|
|
@ -40,42 +29,28 @@ export default function SettingsScreen() {
|
|||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Prüfe den aktuellen Authentifizierungsstatus
|
||||
supabase.auth.getSession().then(({ data: { session } }) => {
|
||||
setSession(session);
|
||||
if (session) getProfile(session);
|
||||
});
|
||||
if (user) loadProfile();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [user]);
|
||||
|
||||
// Abonniere Authentifizierungsänderungen
|
||||
const {
|
||||
data: { subscription },
|
||||
} = supabase.auth.onAuthStateChange((_event, session) => {
|
||||
setSession(session);
|
||||
if (session) getProfile(session);
|
||||
});
|
||||
|
||||
return () => subscription.unsubscribe();
|
||||
}, []);
|
||||
|
||||
async function getProfile(currentSession: Session) {
|
||||
async function loadProfile() {
|
||||
try {
|
||||
setLoading(true);
|
||||
const { user } = currentSession;
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('profiles')
|
||||
.select('*')
|
||||
.eq('id', user.id)
|
||||
.single();
|
||||
const { data, error } = await api.getProfile();
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
if (data) {
|
||||
setProfile(data);
|
||||
setFirstName(data.first_name || '');
|
||||
setLastName(data.last_name || '');
|
||||
setName(data.name || '');
|
||||
}
|
||||
|
||||
// Also load credit balance
|
||||
const { data: creditData } = await api.getCreditBalance();
|
||||
if (creditData) {
|
||||
setCreditBalance(creditData.balance);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
|
|
@ -87,39 +62,15 @@ export default function SettingsScreen() {
|
|||
}
|
||||
|
||||
async function updateProfile() {
|
||||
if (!session) return;
|
||||
if (!user) return;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const { user } = session;
|
||||
|
||||
// Prüfen, ob das Profil bereits existiert
|
||||
if (!profile) {
|
||||
// Erstelle ein neues Profil, wenn es noch nicht existiert
|
||||
const { error: insertError } = await supabase.from('profiles').insert([
|
||||
{
|
||||
id: user.id,
|
||||
first_name: firstName,
|
||||
last_name: lastName,
|
||||
is_individual: true, // Standardmäßig als Einzelnutzer
|
||||
individual_quota: 0,
|
||||
individual_usage: 0,
|
||||
},
|
||||
]);
|
||||
const { error } = await api.updateProfile({ name });
|
||||
|
||||
if (insertError) throw insertError;
|
||||
} else {
|
||||
// Aktualisiere das bestehende Profil
|
||||
const { error: updateError } = await supabase
|
||||
.from('profiles')
|
||||
.update({
|
||||
first_name: firstName,
|
||||
last_name: lastName,
|
||||
updated_at: new Date(),
|
||||
})
|
||||
.eq('id', user.id);
|
||||
|
||||
if (updateError) throw updateError;
|
||||
if (error) {
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
Alert.alert('Erfolg', 'Profil erfolgreich aktualisiert!');
|
||||
|
|
@ -132,11 +83,10 @@ export default function SettingsScreen() {
|
|||
}
|
||||
}
|
||||
|
||||
async function signOut() {
|
||||
async function handleSignOut() {
|
||||
try {
|
||||
setLoading(true);
|
||||
const { error } = await supabase.auth.signOut();
|
||||
if (error) throw error;
|
||||
await signOut();
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
Alert.alert('Fehler beim Abmelden', error.message);
|
||||
|
|
@ -259,44 +209,30 @@ export default function SettingsScreen() {
|
|||
/>
|
||||
<Container>
|
||||
<ScrollView className="flex-1 px-4 py-4">
|
||||
{session && session.user ? (
|
||||
{user ? (
|
||||
<View style={styles.card}>
|
||||
<Text style={styles.sectionTitle}>Mein Profil</Text>
|
||||
|
||||
<View style={styles.formGroup}>
|
||||
<Text style={styles.label}>E-Mail</Text>
|
||||
<Text style={styles.value}>{session?.user?.email}</Text>
|
||||
<Text style={styles.value}>{user.email}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.formGroup}>
|
||||
<Text style={styles.label}>Vorname</Text>
|
||||
<Text style={styles.label}>Name</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={firstName}
|
||||
onChangeText={setFirstName}
|
||||
placeholder="Vorname eingeben"
|
||||
value={name}
|
||||
onChangeText={setName}
|
||||
placeholder="Name eingeben"
|
||||
placeholderTextColor={isDarkMode ? '#9CA3AF' : '#9CA3AF'}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.formGroup}>
|
||||
<Text style={styles.label}>Nachname</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={lastName}
|
||||
onChangeText={setLastName}
|
||||
placeholder="Nachname eingeben"
|
||||
placeholderTextColor={isDarkMode ? '#9CA3AF' : '#9CA3AF'}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{profile && profile.is_individual && (
|
||||
{creditBalance !== null && (
|
||||
<View style={styles.quotaContainer}>
|
||||
<Text style={styles.label}>Verfügbare Kredite</Text>
|
||||
<Text style={styles.quota}>
|
||||
{profile.individual_quota - profile.individual_usage} /{' '}
|
||||
{profile.individual_quota}
|
||||
</Text>
|
||||
<Text style={styles.quota}>{creditBalance}</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
|
|
@ -308,7 +244,7 @@ export default function SettingsScreen() {
|
|||
|
||||
<TouchableOpacity
|
||||
style={[styles.button, styles.signOutButton]}
|
||||
onPress={signOut}
|
||||
onPress={handleSignOut}
|
||||
disabled={loading}
|
||||
>
|
||||
<Text style={styles.buttonText}>Abmelden</Text>
|
||||
|
|
|
|||
|
|
@ -1,21 +1,14 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { Stack, useRouter, useLocalSearchParams } from 'expo-router';
|
||||
import {
|
||||
ScrollView,
|
||||
Text,
|
||||
View,
|
||||
TouchableOpacity,
|
||||
Alert,
|
||||
ActivityIndicator,
|
||||
Modal,
|
||||
} from 'react-native';
|
||||
import { ScrollView, Text, View, TouchableOpacity } from 'react-native';
|
||||
import { FontAwesome5 } from '@expo/vector-icons';
|
||||
|
||||
import { Container } from '~/components/Container';
|
||||
import TeamMembers from '../../../components/TeamMembers';
|
||||
import { useTheme } from '../../../utils/themeContext';
|
||||
import { supabase } from '../../../utils/supabase';
|
||||
import { refreshTeamList } from '../../../components/TeamList';
|
||||
|
||||
// TODO: Team-Details sind in mana-core-auth noch nicht implementiert.
|
||||
// Diese Seite zeigt vorerst die vereinfachte TeamMembers-Komponente an.
|
||||
|
||||
export default function TeamDetails() {
|
||||
const router = useRouter();
|
||||
|
|
@ -25,8 +18,6 @@ export default function TeamDetails() {
|
|||
}>();
|
||||
const { isDarkMode } = useTheme();
|
||||
const [teamName, setTeamName] = useState(initialTeamName || '');
|
||||
const [deletingTeam, setDeletingTeam] = useState(false);
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialTeamName) {
|
||||
|
|
@ -38,149 +29,6 @@ export default function TeamDetails() {
|
|||
router.push('/teams');
|
||||
};
|
||||
|
||||
const deleteTeam = () => {
|
||||
console.log('Delete team button clicked, teamId:', teamId);
|
||||
// Modal öffnen statt Alert anzeigen
|
||||
setShowDeleteModal(true);
|
||||
};
|
||||
|
||||
const confirmDelete = () => {
|
||||
console.log('Löschen bestätigt für Team:', teamId);
|
||||
setShowDeleteModal(false);
|
||||
handleTeamDeletion();
|
||||
};
|
||||
|
||||
const cancelDelete = () => {
|
||||
console.log('Löschen abgebrochen');
|
||||
setShowDeleteModal(false);
|
||||
};
|
||||
|
||||
const handleTeamDeletion = async () => {
|
||||
console.log('Starting deletion process for team:', teamId);
|
||||
setDeletingTeam(true);
|
||||
|
||||
try {
|
||||
// Zuerst alle Abhängigkeiten prüfen
|
||||
console.log('Checking for dependencies...');
|
||||
|
||||
// 1. Prüfe auf credit_transactions
|
||||
const { data: txData } = await supabase
|
||||
.from('credit_transactions')
|
||||
.select('id')
|
||||
.eq('team_id', teamId);
|
||||
|
||||
console.log('Credit transactions:', txData);
|
||||
|
||||
// 2. Prüfe auf team_members
|
||||
const { data: memberData } = await supabase
|
||||
.from('team_members')
|
||||
.select('user_id')
|
||||
.eq('team_id', teamId);
|
||||
|
||||
console.log('Team members:', memberData);
|
||||
|
||||
// 3. Prüfe auf user_roles
|
||||
const { data: roleData } = await supabase
|
||||
.from('user_roles')
|
||||
.select('id')
|
||||
.eq('team_id', teamId);
|
||||
|
||||
console.log('User roles:', roleData);
|
||||
|
||||
// Jetzt versuchen wir, alle Abhängigkeiten zu löschen
|
||||
|
||||
// 1. Lösche credit_transactions
|
||||
if (txData && txData.length > 0) {
|
||||
console.log('Deleting credit transactions...');
|
||||
const { error: txDeleteError } = await supabase
|
||||
.from('credit_transactions')
|
||||
.delete()
|
||||
.eq('team_id', teamId);
|
||||
|
||||
if (txDeleteError) {
|
||||
console.error('Error deleting credit transactions:', txDeleteError);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Lösche team_members
|
||||
if (memberData && memberData.length > 0) {
|
||||
console.log('Deleting team members...');
|
||||
const { error: memberDeleteError } = await supabase
|
||||
.from('team_members')
|
||||
.delete()
|
||||
.eq('team_id', teamId);
|
||||
|
||||
if (memberDeleteError) {
|
||||
console.error('Error deleting team members:', memberDeleteError);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Lösche user_roles
|
||||
if (roleData && roleData.length > 0) {
|
||||
console.log('Deleting user roles...');
|
||||
const { error: roleDeleteError } = await supabase
|
||||
.from('user_roles')
|
||||
.delete()
|
||||
.eq('team_id', teamId);
|
||||
|
||||
if (roleDeleteError) {
|
||||
console.error('Error deleting user roles:', roleDeleteError);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Schließlich das Team löschen
|
||||
console.log('Deleting team...');
|
||||
const { data, error } = await supabase.from('teams').delete().eq('id', teamId);
|
||||
|
||||
console.log('Team deletion response:', { data, error });
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
console.log('Team successfully deleted');
|
||||
|
||||
// Teamliste aktualisieren, wenn der Benutzer authentifiziert ist
|
||||
const {
|
||||
data: { session },
|
||||
} = await supabase.auth.getSession();
|
||||
if (session?.user) {
|
||||
// Teamliste aktualisieren
|
||||
refreshTeamList(session.user.id, () => {
|
||||
console.log('Team list refreshed after deletion');
|
||||
});
|
||||
}
|
||||
|
||||
// Erfolgsmeldung anzeigen und zurück zur Teamliste navigieren
|
||||
Alert.alert('Erfolg', 'Das Team wurde erfolgreich gelöscht.', [
|
||||
{
|
||||
text: 'OK',
|
||||
onPress: () => {
|
||||
console.log('Navigating back to teams list');
|
||||
// Zurück zur Teamliste navigieren
|
||||
router.replace('/teams');
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
// Auch ohne Klick auf OK zurück zur Teamliste navigieren (nach kurzer Verzögerung)
|
||||
setTimeout(() => {
|
||||
router.replace('/teams');
|
||||
}, 1000);
|
||||
} catch (error: any) {
|
||||
console.error('Fehler beim Löschen des Teams:', error);
|
||||
|
||||
// Detaillierte Fehlermeldung anzeigen
|
||||
Alert.alert(
|
||||
'Fehler',
|
||||
`Es ist ein Fehler beim Löschen des Teams aufgetreten: ${error?.message || JSON.stringify(error)}`,
|
||||
[{ text: 'OK' }]
|
||||
);
|
||||
} finally {
|
||||
setDeletingTeam(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen
|
||||
|
|
@ -190,65 +38,6 @@ export default function TeamDetails() {
|
|||
}}
|
||||
/>
|
||||
|
||||
{/* Lösch-Bestätigungsmodal */}
|
||||
<Modal
|
||||
animationType="fade"
|
||||
transparent
|
||||
visible={showDeleteModal}
|
||||
onRequestClose={cancelDelete}
|
||||
>
|
||||
<View
|
||||
className="flex-1 items-center justify-center"
|
||||
style={{ backgroundColor: 'rgba(0,0,0,0.5)' }}
|
||||
>
|
||||
<View
|
||||
className={`m-5 rounded-xl p-6 shadow-lg ${isDarkMode ? 'bg-gray-800' : 'bg-white'}`}
|
||||
style={{ width: '80%' }}
|
||||
>
|
||||
<View className="mb-4 items-center">
|
||||
<FontAwesome5 name="exclamation-triangle" size={40} color="#EF4444" />
|
||||
</View>
|
||||
|
||||
<Text
|
||||
className={`mb-2 text-center text-xl font-bold ${isDarkMode ? 'text-white' : 'text-gray-800'}`}
|
||||
>
|
||||
Team löschen
|
||||
</Text>
|
||||
|
||||
<Text
|
||||
className={`mb-6 text-center text-base ${isDarkMode ? 'text-gray-300' : 'text-gray-600'}`}
|
||||
>
|
||||
{`Möchtest du das Team "${teamName}" wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.`}
|
||||
</Text>
|
||||
|
||||
<View className="flex-row justify-between">
|
||||
<TouchableOpacity
|
||||
className={`mr-2 flex-1 rounded-lg py-3 ${isDarkMode ? 'bg-gray-600' : 'bg-gray-200'}`}
|
||||
onPress={cancelDelete}
|
||||
>
|
||||
<Text
|
||||
className={`text-center font-medium ${isDarkMode ? 'text-white' : 'text-gray-800'}`}
|
||||
>
|
||||
Abbrechen
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
className="ml-2 flex-1 rounded-lg bg-red-600 py-3"
|
||||
onPress={confirmDelete}
|
||||
disabled={deletingTeam}
|
||||
>
|
||||
{deletingTeam ? (
|
||||
<ActivityIndicator size="small" color="white" />
|
||||
) : (
|
||||
<Text className="text-center font-medium text-white">Löschen</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
|
||||
<Container>
|
||||
<ScrollView className="flex-1">
|
||||
<View className="mx-2.5 my-2.5 flex-row justify-between">
|
||||
|
|
@ -268,22 +57,6 @@ export default function TeamDetails() {
|
|||
Zurück zu meinen Teams
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
className={`flex-row items-center rounded-lg px-4 py-2 ${deletingTeam ? 'bg-gray-500' : 'bg-red-600'}`}
|
||||
onPress={deleteTeam} // Öffnet jetzt den Modal-Dialog
|
||||
disabled={deletingTeam}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
{deletingTeam ? (
|
||||
<ActivityIndicator size="small" color="white" />
|
||||
) : (
|
||||
<>
|
||||
<FontAwesome5 name="trash-alt" size={16} color="white" className="mr-2" />
|
||||
<Text className="text-sm font-semibold text-white">Team löschen</Text>
|
||||
</>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<TeamMembers teamId={teamId} />
|
||||
|
|
|
|||
|
|
@ -1,64 +1,44 @@
|
|||
import '../global.css';
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { Stack, useRouter, useSegments } from 'expo-router';
|
||||
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||
import { ThemeProvider } from '~/utils/themeContext';
|
||||
import { supabase } from '../utils/supabase';
|
||||
import { Session } from '@supabase/supabase-js';
|
||||
import { AuthProvider, useAuth } from '~/context/AuthProvider';
|
||||
|
||||
export const unstable_settings = {
|
||||
// Ensure that reloading on `/modal` keeps a back button present.
|
||||
initialRouteName: '(drawer)',
|
||||
};
|
||||
|
||||
// Auth Provider Komponente für die Authentifizierungsprüfung
|
||||
function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
const [session, setSession] = useState<Session | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
// Navigation guard that redirects based on auth state
|
||||
function NavigationGuard({ children }: { children: React.ReactNode }) {
|
||||
const { user, loading } = useAuth();
|
||||
const segments = useSegments();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
// Prüfe den aktuellen Authentifizierungsstatus
|
||||
supabase.auth.getSession().then(({ data: { session } }) => {
|
||||
setSession(session);
|
||||
setIsLoading(false);
|
||||
});
|
||||
|
||||
// Abonniere Authentifizierungsänderungen
|
||||
const {
|
||||
data: { subscription },
|
||||
} = supabase.auth.onAuthStateChange((_event, session) => {
|
||||
setSession(session);
|
||||
setIsLoading(false);
|
||||
});
|
||||
|
||||
return () => subscription.unsubscribe();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading) return;
|
||||
if (loading) return;
|
||||
|
||||
// Prüfe, ob der Benutzer auf der Anmeldeseite oder Passwort-Reset-Seite ist
|
||||
const isLoginScreen = segments[0] === 'login';
|
||||
const isAuthScreen = segments[0] === 'auth'; // For reset-password and other auth routes
|
||||
const isPublicScreen = isLoginScreen || isAuthScreen;
|
||||
|
||||
if (!session && !isPublicScreen) {
|
||||
if (!user && !isPublicScreen) {
|
||||
// Wenn der Benutzer nicht angemeldet ist und nicht auf einer öffentlichen Seite ist,
|
||||
// leite ihn zur Anmeldeseite um
|
||||
router.replace('/login');
|
||||
} else if (session && isLoginScreen) {
|
||||
} else if (user && isLoginScreen) {
|
||||
// Wenn der Benutzer angemeldet ist und auf der Anmeldeseite ist,
|
||||
// leite ihn zur Hauptseite um
|
||||
router.replace('/');
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [session, segments, isLoading]);
|
||||
}, [user, segments, loading]);
|
||||
|
||||
// Zeige nichts während des Ladens
|
||||
if (isLoading) return null;
|
||||
if (loading) return null;
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
|
@ -67,14 +47,16 @@ export default function RootLayout() {
|
|||
return (
|
||||
<ThemeProvider>
|
||||
<AuthProvider>
|
||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||
<Stack>
|
||||
<Stack.Screen name="(drawer)" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="modal" options={{ title: 'Modal', presentation: 'modal' }} />
|
||||
<Stack.Screen name="login" options={{ headerShown: true }} />
|
||||
<Stack.Screen name="auth" options={{ headerShown: false }} />
|
||||
</Stack>
|
||||
</GestureHandlerRootView>
|
||||
<NavigationGuard>
|
||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||
<Stack>
|
||||
<Stack.Screen name="(drawer)" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="modal" options={{ title: 'Modal', presentation: 'modal' }} />
|
||||
<Stack.Screen name="login" options={{ headerShown: true }} />
|
||||
<Stack.Screen name="auth" options={{ headerShown: false }} />
|
||||
</Stack>
|
||||
</GestureHandlerRootView>
|
||||
</NavigationGuard>
|
||||
</AuthProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,48 +1,36 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { View, Text, TextInput, TouchableOpacity, StyleSheet, Alert } from 'react-native';
|
||||
import { supabase } from '../utils/supabase';
|
||||
import { Session } from '@supabase/supabase-js';
|
||||
import { useAuth } from '../context/AuthProvider';
|
||||
import { api } from '../services/api';
|
||||
|
||||
interface Profile {
|
||||
id: string;
|
||||
first_name: string | null;
|
||||
last_name: string | null;
|
||||
avatar_url: string | null;
|
||||
is_individual: boolean;
|
||||
individual_quota: number;
|
||||
individual_usage: number;
|
||||
}
|
||||
|
||||
export default function Account({ session }: { session: Session }) {
|
||||
export default function Account() {
|
||||
const { user, signOut } = useAuth();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [firstName, setFirstName] = useState('');
|
||||
const [lastName, setLastName] = useState('');
|
||||
const [profile, setProfile] = useState<Profile | null>(null);
|
||||
const [name, setName] = useState('');
|
||||
const [creditBalance, setCreditBalance] = useState<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (session) getProfile();
|
||||
if (user) loadProfile();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [session]);
|
||||
}, [user]);
|
||||
|
||||
async function getProfile() {
|
||||
async function loadProfile() {
|
||||
try {
|
||||
setLoading(true);
|
||||
const { user } = session;
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('profiles')
|
||||
.select('*')
|
||||
.eq('id', user.id)
|
||||
.single();
|
||||
const { data, error } = await api.getProfile();
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
if (data) {
|
||||
setProfile(data);
|
||||
setFirstName(data.first_name || '');
|
||||
setLastName(data.last_name || '');
|
||||
setName(data.name || '');
|
||||
}
|
||||
|
||||
// Also load credit balance
|
||||
const { data: creditData } = await api.getCreditBalance();
|
||||
if (creditData) {
|
||||
setCreditBalance(creditData.balance);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
|
|
@ -56,35 +44,11 @@ export default function Account({ session }: { session: Session }) {
|
|||
async function updateProfile() {
|
||||
try {
|
||||
setLoading(true);
|
||||
const { user } = session;
|
||||
|
||||
// Prüfen, ob das Profil bereits existiert
|
||||
if (!profile) {
|
||||
// Erstelle ein neues Profil, wenn es noch nicht existiert
|
||||
const { error: insertError } = await supabase.from('profiles').insert([
|
||||
{
|
||||
id: user.id,
|
||||
first_name: firstName,
|
||||
last_name: lastName,
|
||||
is_individual: true, // Standardmäßig als Einzelnutzer
|
||||
individual_quota: 0,
|
||||
individual_usage: 0,
|
||||
},
|
||||
]);
|
||||
const { error } = await api.updateProfile({ name });
|
||||
|
||||
if (insertError) throw insertError;
|
||||
} else {
|
||||
// Aktualisiere das bestehende Profil
|
||||
const { error: updateError } = await supabase
|
||||
.from('profiles')
|
||||
.update({
|
||||
first_name: firstName,
|
||||
last_name: lastName,
|
||||
updated_at: new Date(),
|
||||
})
|
||||
.eq('id', user.id);
|
||||
|
||||
if (updateError) throw updateError;
|
||||
if (error) {
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
Alert.alert('Erfolg', 'Profil erfolgreich aktualisiert!');
|
||||
|
|
@ -97,11 +61,10 @@ export default function Account({ session }: { session: Session }) {
|
|||
}
|
||||
}
|
||||
|
||||
async function signOut() {
|
||||
async function handleSignOut() {
|
||||
try {
|
||||
setLoading(true);
|
||||
const { error } = await supabase.auth.signOut();
|
||||
if (error) throw error;
|
||||
await signOut();
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
Alert.alert('Fehler beim Abmelden', error.message);
|
||||
|
|
@ -118,35 +81,23 @@ export default function Account({ session }: { session: Session }) {
|
|||
|
||||
<View style={styles.formGroup}>
|
||||
<Text style={styles.label}>E-Mail</Text>
|
||||
<Text style={styles.value}>{session?.user?.email}</Text>
|
||||
<Text style={styles.value}>{user?.email}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.formGroup}>
|
||||
<Text style={styles.label}>Vorname</Text>
|
||||
<Text style={styles.label}>Name</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={firstName}
|
||||
onChangeText={setFirstName}
|
||||
placeholder="Vorname eingeben"
|
||||
value={name}
|
||||
onChangeText={setName}
|
||||
placeholder="Name eingeben"
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.formGroup}>
|
||||
<Text style={styles.label}>Nachname</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={lastName}
|
||||
onChangeText={setLastName}
|
||||
placeholder="Nachname eingeben"
|
||||
/>
|
||||
</View>
|
||||
|
||||
{profile && profile.is_individual && (
|
||||
{creditBalance !== null && (
|
||||
<View style={styles.quotaContainer}>
|
||||
<Text style={styles.label}>Verfügbare Kredite</Text>
|
||||
<Text style={styles.quota}>
|
||||
{profile.individual_quota - profile.individual_usage} / {profile.individual_quota}
|
||||
</Text>
|
||||
<Text style={styles.quota}>{creditBalance}</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
|
|
@ -158,7 +109,7 @@ export default function Account({ session }: { session: Session }) {
|
|||
|
||||
<TouchableOpacity
|
||||
style={[styles.button, styles.signOutButton]}
|
||||
onPress={signOut}
|
||||
onPress={handleSignOut}
|
||||
disabled={loading}
|
||||
>
|
||||
<Text style={styles.buttonText}>Abmelden</Text>
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
|
|
@ -8,168 +8,63 @@ import {
|
|||
ActivityIndicator,
|
||||
ScrollView,
|
||||
} from 'react-native';
|
||||
import { supabase } from '../utils/supabase';
|
||||
import { Session } from '@supabase/supabase-js';
|
||||
import { useAuth } from '../context/AuthProvider';
|
||||
import { api } from '../services/api';
|
||||
import { useTheme } from '../utils/themeContext';
|
||||
|
||||
interface UserRole {
|
||||
role_id: string;
|
||||
roles?: {
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateOrganizationProps {
|
||||
onOrgCreated?: (orgId: string, orgName: string) => void;
|
||||
}
|
||||
|
||||
export default function CreateOrganization({ onOrgCreated }: CreateOrganizationProps) {
|
||||
const [session, setSession] = useState<Session | null>(null);
|
||||
const { user } = useAuth();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [organizationName, setOrganizationName] = useState('');
|
||||
const [initialCredits, setInitialCredits] = useState('');
|
||||
const [userHasPermission, setUserHasPermission] = useState(false);
|
||||
const [checkingPermission, setCheckingPermission] = useState(true);
|
||||
const { isDarkMode } = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
// Prüfe den aktuellen Authentifizierungsstatus
|
||||
supabase.auth.getSession().then(({ data: { session } }) => {
|
||||
setSession(session);
|
||||
if (session) {
|
||||
checkUserPermission(session.user.id);
|
||||
} else {
|
||||
setCheckingPermission(false);
|
||||
}
|
||||
});
|
||||
|
||||
// Abonniere Authentifizierungsänderungen
|
||||
const {
|
||||
data: { subscription },
|
||||
} = supabase.auth.onAuthStateChange((_event, session) => {
|
||||
setSession(session);
|
||||
if (session) {
|
||||
checkUserPermission(session.user.id);
|
||||
} else {
|
||||
setCheckingPermission(false);
|
||||
}
|
||||
});
|
||||
|
||||
return () => subscription.unsubscribe();
|
||||
}, []);
|
||||
|
||||
async function checkUserPermission(userId: string) {
|
||||
try {
|
||||
setCheckingPermission(true);
|
||||
|
||||
// Prüfe, ob der Benutzer ein System-Administrator ist
|
||||
const { data: userRoles, error: userRolesError } = (await supabase
|
||||
.from('user_roles')
|
||||
.select('role_id, roles(name)')
|
||||
.eq('user_id', userId)) as { data: UserRole[] | null; error: any };
|
||||
|
||||
if (userRolesError) throw userRolesError;
|
||||
|
||||
if (userRoles && userRoles.length > 0) {
|
||||
// Prüfe, ob der Benutzer die Rolle "system_admin" hat
|
||||
const isSystemAdmin = userRoles.some((role) => {
|
||||
const roleName = role.roles?.name;
|
||||
return roleName === 'system_admin';
|
||||
});
|
||||
|
||||
setUserHasPermission(isSystemAdmin);
|
||||
} else {
|
||||
setUserHasPermission(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Prüfen der Benutzerberechtigungen:', error);
|
||||
setUserHasPermission(false);
|
||||
} finally {
|
||||
setCheckingPermission(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function createOrganization() {
|
||||
if (!session) {
|
||||
if (!user) {
|
||||
Alert.alert('Fehler', 'Sie müssen angemeldet sein, um eine Organisation zu erstellen.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!userHasPermission) {
|
||||
Alert.alert('Fehler', 'Sie haben keine Berechtigung, Organisationen zu erstellen.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!organizationName.trim()) {
|
||||
Alert.alert('Fehler', 'Bitte geben Sie einen Organisationsnamen ein.');
|
||||
return;
|
||||
}
|
||||
|
||||
const credits = parseInt(initialCredits);
|
||||
if (isNaN(credits) || credits < 0) {
|
||||
Alert.alert('Fehler', 'Bitte geben Sie eine gültige Anzahl an Krediten ein.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// 1. Erstelle die Organisation
|
||||
const { data: organization, error: orgError } = await supabase
|
||||
.from('organizations')
|
||||
.insert([
|
||||
{
|
||||
name: organizationName.trim(),
|
||||
total_credits: credits,
|
||||
used_credits: 0,
|
||||
},
|
||||
])
|
||||
.select()
|
||||
.single();
|
||||
const { data, error } = await api.createOrganization({
|
||||
name: organizationName.trim(),
|
||||
});
|
||||
|
||||
if (orgError) throw orgError;
|
||||
if (error) {
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
// 2. Hole die org_admin Rolle
|
||||
const { data: adminRole, error: roleError } = await supabase
|
||||
.from('roles')
|
||||
.select('id')
|
||||
.eq('name', 'org_admin')
|
||||
.single();
|
||||
|
||||
if (roleError) throw roleError;
|
||||
|
||||
// 3. Füge den aktuellen Benutzer als Organisations-Administrator hinzu
|
||||
const { error: userRoleError } = await supabase.from('user_roles').insert([
|
||||
{
|
||||
user_id: session.user.id,
|
||||
role_id: adminRole.id,
|
||||
organization_id: organization.id,
|
||||
},
|
||||
]);
|
||||
|
||||
if (userRoleError) throw userRoleError;
|
||||
|
||||
// Erfolgsbenachrichtigung anzeigen und direkt navigieren
|
||||
Alert.alert('Erfolg', `Die Organisation "${organizationName}" wurde erfolgreich erstellt.`);
|
||||
|
||||
// Direkt zur Organisationsdetailseite navigieren, ohne auf OK zu warten
|
||||
if (onOrgCreated) {
|
||||
console.log('Navigiere zur neuen Organisationsseite:', organization.id, organization.name);
|
||||
onOrgCreated(organization.id, organization.name);
|
||||
if (onOrgCreated && data) {
|
||||
const orgData = data as any;
|
||||
onOrgCreated(orgData.id || orgData.organizationId, organizationName.trim());
|
||||
}
|
||||
|
||||
// Formular zurücksetzen
|
||||
setOrganizationName('');
|
||||
setInitialCredits('');
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.error('Fehler beim Erstellen der Organisation:', error);
|
||||
Alert.alert('Fehler', 'Es ist ein Fehler beim Erstellen der Organisation aufgetreten.');
|
||||
Alert.alert(
|
||||
'Fehler',
|
||||
error.message || 'Es ist ein Fehler beim Erstellen der Organisation aufgetreten.'
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
if (!user) {
|
||||
return (
|
||||
<View className={`${isDarkMode ? 'bg-gray-800' : 'bg-white'} m-2.5 rounded-lg p-5 shadow`}>
|
||||
<Text
|
||||
|
|
@ -181,32 +76,6 @@ export default function CreateOrganization({ onOrgCreated }: CreateOrganizationP
|
|||
);
|
||||
}
|
||||
|
||||
if (checkingPermission) {
|
||||
return (
|
||||
<View className={`${isDarkMode ? 'bg-gray-800' : 'bg-white'} m-2.5 rounded-lg p-5 shadow`}>
|
||||
<ActivityIndicator size="large" color={isDarkMode ? '#93C5FD' : '#0055FF'} />
|
||||
<Text
|
||||
className={`text-base ${isDarkMode ? 'text-gray-300' : 'text-gray-600'} mt-2.5 text-center`}
|
||||
>
|
||||
Prüfe Berechtigungen...
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (!userHasPermission) {
|
||||
return (
|
||||
<View className={`${isDarkMode ? 'bg-gray-800' : 'bg-white'} m-2.5 rounded-lg p-5 shadow`}>
|
||||
<Text
|
||||
className={`text-base ${isDarkMode ? 'text-gray-300' : 'text-gray-600'} p-5 text-center`}
|
||||
>
|
||||
Sie haben keine Berechtigung, Organisationen zu erstellen. Bitte kontaktieren Sie einen
|
||||
Administrator.
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollView className="flex-1">
|
||||
<View className={`${isDarkMode ? 'bg-gray-800' : 'bg-white'} m-2.5 rounded-lg p-5 shadow`}>
|
||||
|
|
@ -231,26 +100,6 @@ export default function CreateOrganization({ onOrgCreated }: CreateOrganizationP
|
|||
/>
|
||||
</View>
|
||||
|
||||
<View className="mb-4">
|
||||
<Text
|
||||
className={`mb-2 text-base font-medium ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}
|
||||
>
|
||||
Anfängliche Kredite
|
||||
</Text>
|
||||
<TextInput
|
||||
className={`h-12 border ${isDarkMode ? 'border-gray-600 bg-gray-700 text-white' : 'border-gray-300 bg-gray-50 text-gray-800'} rounded-lg px-4 text-base`}
|
||||
value={initialCredits}
|
||||
onChangeText={setInitialCredits}
|
||||
placeholder="Anzahl der Kredite eingeben"
|
||||
placeholderTextColor={isDarkMode ? '#9CA3AF' : '#9CA3AF'}
|
||||
keyboardType="number-pad"
|
||||
/>
|
||||
<Text className={`mt-1 text-sm ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`}>
|
||||
Dies ist die Gesamtanzahl der Kredite, die dieser Organisation zur Verfügung stehen
|
||||
werden.
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
className={`${isDarkMode ? 'bg-blue-700' : 'bg-blue-600'} rounded-lg px-4 py-3 ${loading ? 'opacity-70' : ''}`}
|
||||
onPress={createOrganization}
|
||||
|
|
@ -268,5 +117,3 @@ export default function CreateOrganization({ onOrgCreated }: CreateOrganizationP
|
|||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
// NativeWind wird für das Styling verwendet, daher sind keine StyleSheet-Definitionen erforderlich
|
||||
|
|
|
|||
|
|
@ -1,243 +1,21 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
Alert,
|
||||
ActivityIndicator,
|
||||
ScrollView,
|
||||
} from 'react-native';
|
||||
import { supabase } from '../utils/supabase';
|
||||
import { Session } from '@supabase/supabase-js';
|
||||
import React from 'react';
|
||||
import { View, Text } from 'react-native';
|
||||
import { FontAwesome5 } from '@expo/vector-icons';
|
||||
import { useAuth } from '../context/AuthProvider';
|
||||
import { useTheme } from '../utils/themeContext';
|
||||
import { useRouter } from 'expo-router';
|
||||
|
||||
interface Organization {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface UserRole {
|
||||
organization_id: string;
|
||||
roles?: {
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
// TODO: Team-Erstellung ist in mana-core-auth noch nicht implementiert.
|
||||
// Diese Komponente zeigt vorerst einen Hinweis an.
|
||||
|
||||
interface CreateTeamProps {
|
||||
onTeamCreated?: (teamId: string, teamName: string) => void;
|
||||
}
|
||||
|
||||
export default function CreateTeam({ onTeamCreated }: CreateTeamProps) {
|
||||
const _router = useRouter();
|
||||
const [session, setSession] = useState<Session | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [organizations, setOrganizations] = useState<Organization[]>([]);
|
||||
const [selectedOrgId, setSelectedOrgId] = useState<string | null>(null);
|
||||
const [teamName, setTeamName] = useState('');
|
||||
const [allocatedCredits, setAllocatedCredits] = useState('');
|
||||
const [fetchingOrgs, setFetchingOrgs] = useState(true);
|
||||
export default function CreateTeam({ onTeamCreated: _onTeamCreated }: CreateTeamProps) {
|
||||
const { isDarkMode } = useTheme();
|
||||
const { user } = useAuth();
|
||||
|
||||
useEffect(() => {
|
||||
// Prüfe den aktuellen Authentifizierungsstatus
|
||||
supabase.auth.getSession().then(({ data: { session } }) => {
|
||||
setSession(session);
|
||||
if (session) {
|
||||
fetchUserOrganizations(session.user.id);
|
||||
} else {
|
||||
setFetchingOrgs(false);
|
||||
}
|
||||
});
|
||||
|
||||
// Abonniere Authentifizierungsänderungen
|
||||
const {
|
||||
data: { subscription },
|
||||
} = supabase.auth.onAuthStateChange((_event, session) => {
|
||||
setSession(session);
|
||||
if (session) {
|
||||
fetchUserOrganizations(session.user.id);
|
||||
} else {
|
||||
setFetchingOrgs(false);
|
||||
}
|
||||
});
|
||||
|
||||
return () => subscription.unsubscribe();
|
||||
}, []);
|
||||
|
||||
async function fetchUserOrganizations(userId: string) {
|
||||
try {
|
||||
setFetchingOrgs(true);
|
||||
|
||||
// Suche nach Organisationen, in denen der Benutzer eine Rolle hat
|
||||
const { data: userRoles, error: userRolesError } = (await supabase
|
||||
.from('user_roles')
|
||||
.select('organization_id, roles(name)')
|
||||
.eq('user_id', userId)
|
||||
.not('organization_id', 'is', null)) as { data: UserRole[] | null; error: any };
|
||||
|
||||
if (userRolesError) throw userRolesError;
|
||||
|
||||
if (userRoles && userRoles.length > 0) {
|
||||
// Extrahiere die Organisations-IDs
|
||||
const orgIds = userRoles
|
||||
.filter((role) => {
|
||||
// Prüfe, ob der Benutzer ein Administrator oder Manager ist
|
||||
const roleName = role.roles?.name;
|
||||
return (
|
||||
roleName === 'system_admin' || roleName === 'org_admin' || roleName === 'team_admin'
|
||||
);
|
||||
})
|
||||
.map((role) => role.organization_id);
|
||||
|
||||
if (orgIds.length > 0) {
|
||||
// Hole die Details der Organisationen
|
||||
const { data: orgs, error: orgsError } = await supabase
|
||||
.from('organizations')
|
||||
.select('id, name')
|
||||
.in('id', orgIds);
|
||||
|
||||
if (orgsError) throw orgsError;
|
||||
|
||||
if (orgs && orgs.length > 0) {
|
||||
setOrganizations(orgs);
|
||||
// Setze die erste Organisation als Standard
|
||||
setSelectedOrgId(orgs[0].id);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Abrufen der Organisationen:', error);
|
||||
Alert.alert('Fehler', 'Es ist ein Fehler beim Laden der Organisationen aufgetreten.');
|
||||
} finally {
|
||||
setFetchingOrgs(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function createTeam() {
|
||||
if (!session) {
|
||||
Alert.alert('Fehler', 'Sie müssen angemeldet sein, um ein Team zu erstellen.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selectedOrgId) {
|
||||
Alert.alert('Fehler', 'Bitte wählen Sie eine Organisation aus.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!teamName.trim()) {
|
||||
Alert.alert('Fehler', 'Bitte geben Sie einen Teamnamen ein.');
|
||||
return;
|
||||
}
|
||||
|
||||
const credits = parseInt(allocatedCredits);
|
||||
if (isNaN(credits) || credits < 0) {
|
||||
Alert.alert('Fehler', 'Bitte geben Sie eine gültige Anzahl an Krediten ein.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// 1. Prüfe, ob die Organisation genügend Kredite hat
|
||||
const { data: org, error: orgError } = await supabase
|
||||
.from('organizations')
|
||||
.select('total_credits, used_credits')
|
||||
.eq('id', selectedOrgId)
|
||||
.single();
|
||||
|
||||
if (orgError) throw orgError;
|
||||
|
||||
const availableCredits = org.total_credits - org.used_credits;
|
||||
if (credits > availableCredits) {
|
||||
Alert.alert(
|
||||
'Fehler',
|
||||
`Die Organisation hat nicht genügend Kredite. Verfügbar: ${availableCredits}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Erstelle das Team
|
||||
const { data: team, error: teamError } = await supabase
|
||||
.from('teams')
|
||||
.insert([
|
||||
{
|
||||
organization_id: selectedOrgId,
|
||||
name: teamName.trim(),
|
||||
allocated_credits: credits,
|
||||
},
|
||||
])
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (teamError) throw teamError;
|
||||
|
||||
// 3. Aktualisiere die verwendeten Kredite der Organisation
|
||||
const { error: updateOrgError } = await supabase
|
||||
.from('organizations')
|
||||
.update({
|
||||
used_credits: org.used_credits + credits,
|
||||
})
|
||||
.eq('id', selectedOrgId);
|
||||
|
||||
if (updateOrgError) throw updateOrgError;
|
||||
|
||||
// 4. Füge den aktuellen Benutzer als Team-Administrator hinzu
|
||||
// Zuerst hole die team_admin Rolle
|
||||
const { data: adminRole, error: roleError } = await supabase
|
||||
.from('roles')
|
||||
.select('id')
|
||||
.eq('name', 'team_admin')
|
||||
.single();
|
||||
|
||||
if (roleError) throw roleError;
|
||||
|
||||
// Füge die Benutzerrolle hinzu
|
||||
const { error: userRoleError } = await supabase.from('user_roles').insert([
|
||||
{
|
||||
user_id: session.user.id,
|
||||
role_id: adminRole.id,
|
||||
team_id: team.id,
|
||||
organization_id: selectedOrgId,
|
||||
},
|
||||
]);
|
||||
|
||||
if (userRoleError) throw userRoleError;
|
||||
|
||||
// 5. Füge den Benutzer als Teammitglied hinzu
|
||||
const { error: teamMemberError } = await supabase.from('team_members').insert([
|
||||
{
|
||||
team_id: team.id,
|
||||
user_id: session.user.id,
|
||||
allocated_credits: 0, // Standardmäßig keine Kredite zugewiesen
|
||||
used_credits: 0,
|
||||
},
|
||||
]);
|
||||
|
||||
if (teamMemberError) throw teamMemberError;
|
||||
|
||||
// Erfolgsbenachrichtigung anzeigen und direkt navigieren
|
||||
Alert.alert('Erfolg', `Das Team "${teamName}" wurde erfolgreich erstellt.`);
|
||||
|
||||
// Direkt zur Teamdetailseite navigieren, ohne auf OK zu warten
|
||||
if (onTeamCreated) {
|
||||
console.log('Navigiere zur neuen Teamseite:', team.id, team.name);
|
||||
onTeamCreated(team.id, team.name);
|
||||
}
|
||||
|
||||
// Formular zurücksetzen
|
||||
setTeamName('');
|
||||
setAllocatedCredits('');
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Erstellen des Teams:', error);
|
||||
Alert.alert('Fehler', 'Es ist ein Fehler beim Erstellen des Teams aufgetreten.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
if (!user) {
|
||||
return (
|
||||
<View className={`${isDarkMode ? 'bg-gray-800' : 'bg-white'} m-2.5 rounded-lg p-5 shadow`}>
|
||||
<Text
|
||||
|
|
@ -249,108 +27,21 @@ export default function CreateTeam({ onTeamCreated }: CreateTeamProps) {
|
|||
);
|
||||
}
|
||||
|
||||
if (fetchingOrgs) {
|
||||
return (
|
||||
<View className={`${isDarkMode ? 'bg-gray-800' : 'bg-white'} m-2.5 rounded-lg p-5 shadow`}>
|
||||
<ActivityIndicator size="large" color={isDarkMode ? '#93C5FD' : '#0055FF'} />
|
||||
<Text
|
||||
className={`text-base ${isDarkMode ? 'text-gray-300' : 'text-gray-600'} mt-2.5 text-center`}
|
||||
>
|
||||
Lade Organisationen...
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (organizations.length === 0) {
|
||||
return (
|
||||
<View className={`${isDarkMode ? 'bg-gray-800' : 'bg-white'} m-2.5 rounded-lg p-5 shadow`}>
|
||||
<Text
|
||||
className={`text-base ${isDarkMode ? 'text-gray-300' : 'text-gray-600'} p-5 text-center`}
|
||||
>
|
||||
Sie haben keine Berechtigung, Teams zu erstellen, oder Sie gehören keiner Organisation an.
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollView className="flex-1">
|
||||
<View className={`${isDarkMode ? 'bg-gray-800' : 'bg-white'} m-2.5 rounded-lg p-5 shadow`}>
|
||||
<View className={`${isDarkMode ? 'bg-gray-800' : 'bg-white'} m-2.5 rounded-lg p-5 shadow`}>
|
||||
<Text className={`mb-5 text-2xl font-bold ${isDarkMode ? 'text-gray-100' : 'text-gray-800'}`}>
|
||||
Neues Team erstellen
|
||||
</Text>
|
||||
|
||||
<View className="items-center py-8">
|
||||
<FontAwesome5 name="users" size={50} color={isDarkMode ? '#4B5563' : '#9CA3AF'} />
|
||||
<Text
|
||||
className={`mb-5 text-2xl font-bold ${isDarkMode ? 'text-gray-100' : 'text-gray-800'}`}
|
||||
className={`mt-4 text-center text-base ${isDarkMode ? 'text-gray-300' : 'text-gray-600'}`}
|
||||
>
|
||||
Neues Team erstellen
|
||||
Die Team-Erstellung wird derzeit auf das neue Auth-System migriert und ist bald wieder
|
||||
verfügbar.
|
||||
</Text>
|
||||
|
||||
<View className="mb-4">
|
||||
<Text
|
||||
className={`mb-2 text-base font-medium ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}
|
||||
>
|
||||
Organisation
|
||||
</Text>
|
||||
<View className="mb-2.5 flex-row flex-wrap">
|
||||
{organizations.map((org) => (
|
||||
<TouchableOpacity
|
||||
key={org.id}
|
||||
className={`${selectedOrgId === org.id ? (isDarkMode ? 'bg-blue-800' : 'bg-blue-600') : isDarkMode ? 'bg-gray-700' : 'bg-gray-100'} mb-2.5 mr-2.5 rounded-lg px-4 py-2.5`}
|
||||
onPress={() => setSelectedOrgId(org.id)}
|
||||
>
|
||||
<Text
|
||||
className={`${selectedOrgId === org.id ? 'text-white' : isDarkMode ? 'text-gray-300' : 'text-gray-700'} font-medium`}
|
||||
>
|
||||
{org.name}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className="mb-4">
|
||||
<Text
|
||||
className={`mb-2 text-base font-medium ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}
|
||||
>
|
||||
Teamname
|
||||
</Text>
|
||||
<TextInput
|
||||
className={`h-12 border ${isDarkMode ? 'border-gray-600 bg-gray-700 text-white' : 'border-gray-300 bg-gray-50 text-gray-800'} rounded-lg px-4 text-base`}
|
||||
value={teamName}
|
||||
onChangeText={setTeamName}
|
||||
placeholder="Name des Teams eingeben"
|
||||
placeholderTextColor={isDarkMode ? '#9CA3AF' : '#9CA3AF'}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View className="mb-4">
|
||||
<Text
|
||||
className={`mb-2 text-base font-medium ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}
|
||||
>
|
||||
Zugewiesene Kredite
|
||||
</Text>
|
||||
<TextInput
|
||||
className={`h-12 border ${isDarkMode ? 'border-gray-600 bg-gray-700 text-white' : 'border-gray-300 bg-gray-50 text-gray-800'} rounded-lg px-4 text-base`}
|
||||
value={allocatedCredits}
|
||||
onChangeText={setAllocatedCredits}
|
||||
placeholder="Anzahl der Kredite eingeben"
|
||||
placeholderTextColor={isDarkMode ? '#9CA3AF' : '#9CA3AF'}
|
||||
keyboardType="number-pad"
|
||||
/>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
className={`mt-2.5 h-12 items-center justify-center rounded-lg ${loading ? (isDarkMode ? 'bg-gray-600' : 'bg-gray-400') : isDarkMode ? 'bg-blue-700' : 'bg-blue-600'}`}
|
||||
onPress={createTeam}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator color="#fff" size="small" />
|
||||
) : (
|
||||
<Text className="text-base font-bold text-white">Team erstellen</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// NativeWind wird für das Styling verwendet, daher sind keine StyleSheet-Definitionen erforderlich
|
||||
|
|
|
|||
|
|
@ -2,89 +2,41 @@ import React, { useState, useEffect } from 'react';
|
|||
import { View, Text, TouchableOpacity, ActivityIndicator } from 'react-native';
|
||||
import { FontAwesome5 } from '@expo/vector-icons';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { supabase } from '../utils/supabase';
|
||||
import { Session } from '@supabase/supabase-js';
|
||||
import { useAuth } from '../context/AuthProvider';
|
||||
import { api } from '../services/api';
|
||||
import { useTheme, lightColors, darkColors } from '../utils/themeContext';
|
||||
|
||||
export default function DashboardStats() {
|
||||
const router = useRouter();
|
||||
const { isDarkMode } = useTheme();
|
||||
const [session, setSession] = useState<Session | null>(null);
|
||||
const { user } = useAuth();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [teamCount, setTeamCount] = useState(0);
|
||||
const [orgCount, setOrgCount] = useState(0);
|
||||
const [availableMana, setAvailableMana] = useState(0);
|
||||
const [totalMana, setTotalMana] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
// Prüfe den aktuellen Authentifizierungsstatus
|
||||
supabase.auth.getSession().then(({ data: { session } }) => {
|
||||
setSession(session);
|
||||
if (session) {
|
||||
fetchUserStats(session.user.id);
|
||||
} else {
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
if (user) {
|
||||
fetchUserStats();
|
||||
} else {
|
||||
setLoading(false);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [user]);
|
||||
|
||||
// Abonniere Authentifizierungsänderungen
|
||||
const {
|
||||
data: { subscription },
|
||||
} = supabase.auth.onAuthStateChange((_event, session) => {
|
||||
setSession(session);
|
||||
if (session) {
|
||||
fetchUserStats(session.user.id);
|
||||
} else {
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
return () => subscription.unsubscribe();
|
||||
}, []);
|
||||
|
||||
async function fetchUserStats(userId: string) {
|
||||
async function fetchUserStats() {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// Hole alle Teams, in denen der Benutzer Mitglied ist
|
||||
const { data: teamMembers, error: teamMembersError } = await supabase
|
||||
.from('team_members')
|
||||
.select('team_id')
|
||||
.eq('user_id', userId);
|
||||
// Fetch organizations count
|
||||
const { data: orgsData } = await api.getOrganizations();
|
||||
setOrgCount(orgsData?.length || 0);
|
||||
|
||||
if (teamMembersError) throw teamMembersError;
|
||||
|
||||
setTeamCount(teamMembers?.length || 0);
|
||||
|
||||
// Hole alle Organisationen, in denen der Benutzer eine Rolle hat
|
||||
const { data: userRoles, error: userRolesError } = await supabase
|
||||
.from('user_roles')
|
||||
.select('organization_id')
|
||||
.eq('user_id', userId)
|
||||
.not('organization_id', 'is', null);
|
||||
|
||||
if (userRolesError) throw userRolesError;
|
||||
|
||||
// Entferne Duplikate (falls der Benutzer mehrere Rollen in einer Organisation hat)
|
||||
const uniqueOrgIds = [...new Set(userRoles?.map((role) => role.organization_id) || [])];
|
||||
setOrgCount(uniqueOrgIds.length);
|
||||
|
||||
// Hole die Mana-Informationen aus dem Profil des Benutzers
|
||||
const { data: profileData, error: profileError } = await supabase
|
||||
.from('profiles')
|
||||
.select('individual_quota, individual_usage')
|
||||
.eq('id', userId)
|
||||
.single();
|
||||
|
||||
if (profileError) throw profileError;
|
||||
|
||||
if (profileData) {
|
||||
const quota = profileData.individual_quota || 0;
|
||||
const usage = profileData.individual_usage || 0;
|
||||
const available = Math.max(0, quota - usage);
|
||||
|
||||
setTotalMana(quota);
|
||||
setAvailableMana(available);
|
||||
// Fetch credit balance
|
||||
const { data: creditData } = await api.getCreditBalance();
|
||||
if (creditData) {
|
||||
setTotalMana(creditData.totalCredits || 0);
|
||||
setAvailableMana(creditData.balance || 0);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Abrufen der Benutzerstatistiken:', error);
|
||||
|
|
@ -93,7 +45,7 @@ export default function DashboardStats() {
|
|||
}
|
||||
}
|
||||
|
||||
if (!session || loading) {
|
||||
if (!user || loading) {
|
||||
return (
|
||||
<View
|
||||
className={`flex-row justify-between ${isDarkMode ? 'bg-gray-800' : 'bg-white'} mb-5 rounded-lg p-4 shadow`}
|
||||
|
|
@ -135,40 +87,12 @@ export default function DashboardStats() {
|
|||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Teams und Organisationen */}
|
||||
{/* Organisationen */}
|
||||
<View
|
||||
className={`flex-row justify-between ${isDarkMode ? 'bg-gray-800' : 'bg-white'} rounded-lg p-4 shadow`}
|
||||
>
|
||||
<TouchableOpacity
|
||||
className={`flex-1 flex-row items-center justify-center ${isDarkMode ? 'bg-gray-700' : 'bg-gray-50'} mr-2 rounded-lg p-3`}
|
||||
onPress={() => router.push('/teams')}
|
||||
>
|
||||
<View className="items-center">
|
||||
<View className="mb-1 flex-row items-center">
|
||||
<FontAwesome5
|
||||
name="users"
|
||||
size={16}
|
||||
color={isDarkMode ? '#60A5FA' : '#0055FF'}
|
||||
className="mr-2"
|
||||
/>
|
||||
<Text
|
||||
className={`text-base font-semibold ${isDarkMode ? 'text-gray-100' : 'text-gray-800'}`}
|
||||
>
|
||||
Teams
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
className={`${isDarkMode ? 'bg-blue-900' : 'bg-blue-100'} rounded-full px-3 py-1`}
|
||||
>
|
||||
<Text className={`${isDarkMode ? 'text-blue-300' : 'text-blue-700'} font-medium`}>
|
||||
{teamCount}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
className={`flex-1 flex-row items-center justify-center ${isDarkMode ? 'bg-gray-700' : 'bg-gray-50'} ml-2 rounded-lg p-3`}
|
||||
className={`flex-1 flex-row items-center justify-center ${isDarkMode ? 'bg-gray-700' : 'bg-gray-50'} rounded-lg p-3`}
|
||||
onPress={() => router.push('/organizations')}
|
||||
>
|
||||
<View className="items-center">
|
||||
|
|
|
|||
|
|
@ -1,19 +1,18 @@
|
|||
import React, { useState, useEffect, forwardRef, useImperativeHandle } from 'react';
|
||||
import { View, Text, FlatList, TouchableOpacity, ActivityIndicator, Alert } from 'react-native';
|
||||
import { FontAwesome5 } from '@expo/vector-icons';
|
||||
import { supabase } from '../utils/supabase';
|
||||
import { Session } from '@supabase/supabase-js';
|
||||
import { useAuth } from '../context/AuthProvider';
|
||||
import { api } from '../services/api';
|
||||
import { useTheme } from '../utils/themeContext';
|
||||
import { useRouter } from 'expo-router';
|
||||
|
||||
interface Organization {
|
||||
id: string;
|
||||
name: string;
|
||||
total_credits: number;
|
||||
used_credits: number;
|
||||
created_at: string;
|
||||
team_count?: number;
|
||||
user_role?: string;
|
||||
slug: string;
|
||||
logo?: string;
|
||||
createdAt: string;
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
interface OrganizationListProps {
|
||||
|
|
@ -27,7 +26,7 @@ interface OrganizationListRef {
|
|||
|
||||
const OrganizationList = forwardRef<OrganizationListRef, OrganizationListProps>(
|
||||
({ hideTitle = false }, ref) => {
|
||||
const [session, setSession] = useState<Session | null>(null);
|
||||
const { user } = useAuth();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [organizations, setOrganizations] = useState<Organization[]>([]);
|
||||
const { isDarkMode } = useTheme();
|
||||
|
|
@ -36,110 +35,32 @@ const OrganizationList = forwardRef<OrganizationListRef, OrganizationListProps>(
|
|||
// Stelle die refreshOrganizations-Methode über die Ref zur Verfügung
|
||||
useImperativeHandle(ref, () => ({
|
||||
refreshOrganizations: () => {
|
||||
if (session) {
|
||||
console.log('Aktualisiere Organisationsliste');
|
||||
fetchUserOrganizations(session.user.id);
|
||||
if (user) {
|
||||
fetchUserOrganizations();
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
// Prüfe den aktuellen Authentifizierungsstatus
|
||||
supabase.auth.getSession().then(({ data: { session } }) => {
|
||||
setSession(session);
|
||||
if (session) {
|
||||
fetchUserOrganizations(session.user.id);
|
||||
} else {
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
// Abonniere Authentifizierungsänderungen
|
||||
const {
|
||||
data: { subscription },
|
||||
} = supabase.auth.onAuthStateChange((_event, session) => {
|
||||
setSession(session);
|
||||
if (session) {
|
||||
fetchUserOrganizations(session.user.id);
|
||||
} else {
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
return () => subscription.unsubscribe();
|
||||
if (user) {
|
||||
fetchUserOrganizations();
|
||||
} else {
|
||||
setLoading(false);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
}, [user]);
|
||||
|
||||
async function fetchUserOrganizations(userId: string) {
|
||||
async function fetchUserOrganizations() {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// Hole alle Organisationen, in denen der Benutzer eine Rolle hat
|
||||
const { data: userRoles, error: userRolesError } = (await supabase
|
||||
.from('user_roles')
|
||||
.select('organization_id, role_id, roles(name)')
|
||||
.eq('user_id', userId)
|
||||
.not('organization_id', 'is', null)) as {
|
||||
data: Array<{
|
||||
organization_id: string;
|
||||
role_id: string;
|
||||
roles: { name: string };
|
||||
}> | null;
|
||||
error: any;
|
||||
};
|
||||
const { data, error } = await api.getOrganizations();
|
||||
|
||||
if (userRolesError) throw userRolesError;
|
||||
|
||||
if (userRoles && userRoles.length > 0) {
|
||||
// Extrahiere die Organisations-IDs
|
||||
const orgIds = [...new Set(userRoles.map((role) => role.organization_id))];
|
||||
|
||||
// Hole die Organisations-Details
|
||||
const { data: orgsData, error: orgsError } = await supabase
|
||||
.from('organizations')
|
||||
.select('id, name, total_credits, used_credits, created_at')
|
||||
.in('id', orgIds);
|
||||
|
||||
if (orgsError) throw orgsError;
|
||||
|
||||
if (orgsData && orgsData.length > 0) {
|
||||
// Hole die Anzahl der Teams für jede Organisation
|
||||
const orgsWithTeamCount = await Promise.all(
|
||||
orgsData.map(async (org) => {
|
||||
// Finde die Rolle des Benutzers in dieser Organisation
|
||||
const userRolesInOrg = userRoles.filter((role) => role.organization_id === org.id);
|
||||
const highestRole = findHighestRole(userRolesInOrg);
|
||||
|
||||
// Hole die Anzahl der Teams in dieser Organisation
|
||||
const { count, error: teamCountError } = await supabase
|
||||
.from('teams')
|
||||
.select('id', { count: 'exact', head: true })
|
||||
.eq('organization_id', org.id);
|
||||
|
||||
if (teamCountError) {
|
||||
console.error('Fehler beim Abrufen der Team-Anzahl:', teamCountError);
|
||||
return {
|
||||
...org,
|
||||
team_count: 0,
|
||||
user_role: highestRole,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...org,
|
||||
team_count: count || 0,
|
||||
user_role: highestRole,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
setOrganizations(orgsWithTeamCount);
|
||||
} else {
|
||||
setOrganizations([]);
|
||||
}
|
||||
} else {
|
||||
setOrganizations([]);
|
||||
if (error) {
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
setOrganizations(data || []);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Abrufen der Organisationen:', error);
|
||||
Alert.alert('Fehler', 'Es ist ein Fehler beim Laden Ihrer Organisationen aufgetreten.');
|
||||
|
|
@ -148,33 +69,6 @@ const OrganizationList = forwardRef<OrganizationListRef, OrganizationListProps>(
|
|||
}
|
||||
}
|
||||
|
||||
// Hilfsfunktion, um die höchste Rolle eines Benutzers zu finden
|
||||
function findHighestRole(
|
||||
userRoles: Array<{ role_id: string; roles: { name: string } }>
|
||||
): string {
|
||||
const roleHierarchy = {
|
||||
system_admin: 4,
|
||||
org_admin: 3,
|
||||
team_admin: 2,
|
||||
member: 1,
|
||||
};
|
||||
|
||||
let highestRoleName = 'member';
|
||||
let highestRoleValue = 0;
|
||||
|
||||
userRoles.forEach((role) => {
|
||||
const roleName = role.roles?.name;
|
||||
if (roleName && roleHierarchy[roleName as keyof typeof roleHierarchy] > highestRoleValue) {
|
||||
highestRoleName = roleName;
|
||||
highestRoleValue = roleHierarchy[roleName as keyof typeof roleHierarchy];
|
||||
}
|
||||
});
|
||||
|
||||
return highestRoleName;
|
||||
}
|
||||
|
||||
// Keine Aufklappfunktion mehr benötigt
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('de-DE', {
|
||||
|
|
@ -184,22 +78,7 @@ const OrganizationList = forwardRef<OrganizationListRef, OrganizationListProps>(
|
|||
});
|
||||
};
|
||||
|
||||
// Diese Funktion wird nicht mehr benötigt, da wir direkt mit NativeWind-Klassen arbeiten
|
||||
|
||||
const getRoleName = (role: string) => {
|
||||
switch (role) {
|
||||
case 'system_admin':
|
||||
return 'System-Admin';
|
||||
case 'org_admin':
|
||||
return 'Organisations-Admin';
|
||||
case 'team_admin':
|
||||
return 'Team-Admin';
|
||||
default:
|
||||
return 'Mitglied';
|
||||
}
|
||||
};
|
||||
|
||||
if (!session) {
|
||||
if (!user) {
|
||||
return (
|
||||
<View
|
||||
className={`flex-1 ${isDarkMode ? 'bg-gray-800' : 'bg-white'} m-2.5 rounded-lg p-4 shadow`}
|
||||
|
|
@ -271,7 +150,6 @@ const OrganizationList = forwardRef<OrganizationListRef, OrganizationListProps>(
|
|||
renderItem={({ item }) => (
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
console.log('Organisation angeklickt:', item.id, item.name);
|
||||
router.push({
|
||||
pathname: '/organizations/[id]',
|
||||
params: { id: item.id, name: item.name },
|
||||
|
|
@ -295,85 +173,19 @@ const OrganizationList = forwardRef<OrganizationListRef, OrganizationListProps>(
|
|||
{item.name}
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
className={`rounded-full px-2 py-0.5 ${item.user_role === 'system_admin' ? 'bg-red-600' : item.user_role === 'org_admin' ? 'bg-orange-500' : item.user_role === 'team_admin' ? 'bg-green-600' : 'bg-gray-600'}`}
|
||||
>
|
||||
<Text className="text-xs font-medium text-white">
|
||||
{getRoleName(item.user_role || 'member')}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Mittlerer Bereich mit Teams und Erstellungsdatum */}
|
||||
<View className="mb-3 flex-row items-center justify-between">
|
||||
<View className="flex-row items-center">
|
||||
<FontAwesome5
|
||||
name="users"
|
||||
size={14}
|
||||
color={isDarkMode ? '#9CA3AF' : '#666'}
|
||||
className="mr-2"
|
||||
/>
|
||||
<Text className={`text-sm ${isDarkMode ? 'text-gray-300' : 'text-gray-600'}`}>
|
||||
{item.team_count} {item.team_count === 1 ? 'Team' : 'Teams'}
|
||||
</Text>
|
||||
</View>
|
||||
<View className="flex-row items-center">
|
||||
<FontAwesome5
|
||||
name="calendar-alt"
|
||||
size={14}
|
||||
color={isDarkMode ? '#9CA3AF' : '#666'}
|
||||
className="mr-2"
|
||||
/>
|
||||
<Text className={`text-sm ${isDarkMode ? 'text-gray-300' : 'text-gray-600'}`}>
|
||||
{formatDate(item.created_at)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Unterer Bereich mit Kreditinformationen */}
|
||||
<View
|
||||
className={`mt-1 flex-row justify-between border-t pt-2 ${isDarkMode ? 'border-gray-600' : 'border-gray-200'}`}
|
||||
>
|
||||
<View className="flex-1 items-center">
|
||||
<Text
|
||||
className={`text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-600'} mb-1`}
|
||||
>
|
||||
Gesamte Kredite
|
||||
</Text>
|
||||
<Text
|
||||
className={`text-base font-semibold ${isDarkMode ? 'text-gray-100' : 'text-gray-800'}`}
|
||||
>
|
||||
{item.total_credits}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View
|
||||
className={`flex-1 items-center border-x ${isDarkMode ? 'border-gray-600' : 'border-gray-200'}`}
|
||||
>
|
||||
<Text
|
||||
className={`text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-600'} mb-1`}
|
||||
>
|
||||
Verwendete Kredite
|
||||
</Text>
|
||||
<Text
|
||||
className={`text-base font-semibold ${isDarkMode ? 'text-gray-100' : 'text-gray-800'}`}
|
||||
>
|
||||
{item.used_credits}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View className="flex-1 items-center">
|
||||
<Text
|
||||
className={`text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-600'} mb-1`}
|
||||
>
|
||||
Verfügbar
|
||||
</Text>
|
||||
<Text
|
||||
className={`text-base font-semibold ${item.total_credits - item.used_credits > 0 ? (isDarkMode ? 'text-green-400' : 'text-green-600') : isDarkMode ? 'text-red-400' : 'text-red-600'}`}
|
||||
>
|
||||
{item.total_credits - item.used_credits}
|
||||
</Text>
|
||||
</View>
|
||||
{/* Erstellungsdatum */}
|
||||
<View className="flex-row items-center">
|
||||
<FontAwesome5
|
||||
name="calendar-alt"
|
||||
size={14}
|
||||
color={isDarkMode ? '#9CA3AF' : '#666'}
|
||||
className="mr-2"
|
||||
/>
|
||||
<Text className={`text-sm ${isDarkMode ? 'text-gray-300' : 'text-gray-600'}`}>
|
||||
{formatDate(item.createdAt)}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
|
@ -384,7 +196,4 @@ const OrganizationList = forwardRef<OrganizationListRef, OrganizationListProps>(
|
|||
}
|
||||
);
|
||||
|
||||
// NativeWind wird für das Styling verwendet, daher sind keine StyleSheet-Definitionen erforderlich
|
||||
|
||||
// Exportiere die Komponente mit forwardRef
|
||||
export default OrganizationList;
|
||||
|
|
|
|||
|
|
@ -1,91 +1,29 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
StyleSheet,
|
||||
Alert,
|
||||
ActivityIndicator,
|
||||
} from 'react-native';
|
||||
import { supabase } from '../utils/supabase';
|
||||
import { Session } from '@supabase/supabase-js';
|
||||
import { View, Text, StyleSheet } from 'react-native';
|
||||
import { useAuth } from '../context/AuthProvider';
|
||||
import { api } from '../services/api';
|
||||
import { useTheme } from '../utils/themeContext';
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
first_name?: string;
|
||||
last_name?: string;
|
||||
}
|
||||
// TODO: Mana-Übertragung zwischen Benutzern ist in mana-core-auth noch nicht implementiert.
|
||||
// Diese Komponente zeigt vorerst nur den aktuellen Kontostand an.
|
||||
|
||||
export default function SendMana() {
|
||||
const { isDarkMode } = useTheme();
|
||||
const [session, setSession] = useState<Session | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [searchLoading, setSearchLoading] = useState(false);
|
||||
const [recipientEmail, setRecipientEmail] = useState('');
|
||||
const [manaAmount, setManaAmount] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
const [foundUser, setFoundUser] = useState<User | null>(null);
|
||||
const { user } = useAuth();
|
||||
const [userCredits, setUserCredits] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
// Prüfe den aktuellen Authentifizierungsstatus
|
||||
supabase.auth.getSession().then(({ data: { session } }) => {
|
||||
setSession(session);
|
||||
if (session) {
|
||||
fetchUserCredits(session.user.id);
|
||||
}
|
||||
});
|
||||
if (user) {
|
||||
fetchUserCredits();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [user]);
|
||||
|
||||
// Abonniere Authentifizierungsänderungen
|
||||
const {
|
||||
data: { subscription },
|
||||
} = supabase.auth.onAuthStateChange((_event, session) => {
|
||||
setSession(session);
|
||||
if (session) {
|
||||
fetchUserCredits(session.user.id);
|
||||
}
|
||||
});
|
||||
|
||||
return () => subscription.unsubscribe();
|
||||
}, []);
|
||||
|
||||
async function fetchUserCredits(userId: string) {
|
||||
async function fetchUserCredits() {
|
||||
try {
|
||||
// Zuerst prüfen wir, ob der Benutzer ein Einzelnutzer ist
|
||||
const { data: profileData, error: profileError } = await supabase
|
||||
.from('profiles')
|
||||
.select('is_individual, individual_quota, individual_usage')
|
||||
.eq('id', userId)
|
||||
.single();
|
||||
|
||||
if (profileError) throw profileError;
|
||||
|
||||
if (profileData && profileData.is_individual) {
|
||||
// Wenn der Benutzer ein Einzelnutzer ist, verwenden wir die individuellen Kredite
|
||||
setUserCredits(profileData.individual_quota - profileData.individual_usage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Wenn der Benutzer kein Einzelnutzer ist, prüfen wir die Teamzugehörigkeit
|
||||
const { data: teamMemberData, error: teamMemberError } = await supabase
|
||||
.from('team_members')
|
||||
.select('allocated_credits, used_credits')
|
||||
.eq('user_id', userId);
|
||||
|
||||
if (teamMemberError) throw teamMemberError;
|
||||
|
||||
if (teamMemberData && teamMemberData.length > 0) {
|
||||
// Berechne die Summe der verfügbaren Kredite aus allen Teams
|
||||
const availableCredits = teamMemberData.reduce((total, member) => {
|
||||
return total + (member.allocated_credits - member.used_credits);
|
||||
}, 0);
|
||||
|
||||
setUserCredits(availableCredits);
|
||||
} else {
|
||||
setUserCredits(0);
|
||||
const { data } = await api.getCreditBalance();
|
||||
if (data) {
|
||||
setUserCredits(data.balance || 0);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Abrufen der Kredite:', error);
|
||||
|
|
@ -93,151 +31,6 @@ export default function SendMana() {
|
|||
}
|
||||
}
|
||||
|
||||
async function searchUser() {
|
||||
if (!recipientEmail.trim()) {
|
||||
Alert.alert('Fehler', 'Bitte geben Sie eine E-Mail-Adresse ein.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setSearchLoading(true);
|
||||
|
||||
// In einer realen Anwendung würden wir hier eine sichere Methode verwenden,
|
||||
// um Benutzer anhand ihrer E-Mail-Adresse zu finden, z.B. über eine spezielle API
|
||||
// oder eine Supabase-Funktion. Für dieses Beispiel simulieren wir die Suche.
|
||||
|
||||
// Suche nach dem Profil mit einer bestimmten E-Mail
|
||||
// Hinweis: In einer echten Anwendung würde dies über eine sichere API erfolgen
|
||||
const { data: profiles, error: profileError } = await supabase
|
||||
.from('profiles')
|
||||
.select('id, first_name, last_name')
|
||||
.limit(1);
|
||||
|
||||
if (profileError || !profiles || profiles.length === 0) {
|
||||
Alert.alert('Fehler', 'Benutzer nicht gefunden.');
|
||||
setFoundUser(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Für Demonstrationszwecke verwenden wir das erste gefundene Profil
|
||||
// In einer realen Anwendung würden wir natürlich das richtige Profil finden
|
||||
const profile = profiles[0];
|
||||
|
||||
setFoundUser({
|
||||
id: profile.id,
|
||||
email: recipientEmail.trim(),
|
||||
first_name: profile.first_name || undefined,
|
||||
last_name: profile.last_name || undefined,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Fehler bei der Benutzersuche:', error);
|
||||
Alert.alert('Fehler', 'Es ist ein Fehler aufgetreten. Bitte versuchen Sie es später erneut.');
|
||||
} finally {
|
||||
setSearchLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function sendMana() {
|
||||
if (!session) {
|
||||
Alert.alert('Fehler', 'Sie müssen angemeldet sein, um Mana zu senden.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!foundUser) {
|
||||
Alert.alert('Fehler', 'Bitte suchen Sie zuerst nach einem Empfänger.');
|
||||
return;
|
||||
}
|
||||
|
||||
const amount = parseInt(manaAmount);
|
||||
if (isNaN(amount) || amount <= 0) {
|
||||
Alert.alert('Fehler', 'Bitte geben Sie einen gültigen Mana-Betrag ein.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (amount > userCredits) {
|
||||
Alert.alert('Fehler', 'Sie haben nicht genügend Mana-Kredite.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// 1. Erstelle einen Eintrag in der credit_transactions Tabelle
|
||||
const { error: transactionError } = await supabase.from('credit_transactions').insert([
|
||||
{
|
||||
user_id: foundUser.id,
|
||||
amount: amount,
|
||||
description: description || 'Mana-Übertragung',
|
||||
app_id: 'manacore',
|
||||
},
|
||||
]);
|
||||
|
||||
if (transactionError) throw transactionError;
|
||||
|
||||
// 2. Aktualisiere die Kredite des Empfängers
|
||||
const { data: recipientProfile, error: recipientProfileError } = await supabase
|
||||
.from('profiles')
|
||||
.select('is_individual, individual_quota')
|
||||
.eq('id', foundUser.id)
|
||||
.single();
|
||||
|
||||
if (recipientProfileError) throw recipientProfileError;
|
||||
|
||||
if (recipientProfile && recipientProfile.is_individual) {
|
||||
// Wenn der Empfänger ein Einzelnutzer ist, erhöhen wir sein Kontingent
|
||||
const { error: updateError } = await supabase
|
||||
.from('profiles')
|
||||
.update({
|
||||
individual_quota: recipientProfile.individual_quota + amount,
|
||||
})
|
||||
.eq('id', foundUser.id);
|
||||
|
||||
if (updateError) throw updateError;
|
||||
}
|
||||
|
||||
// 3. Aktualisiere die Kredite des Senders
|
||||
const { data: senderProfile, error: senderProfileError } = await supabase
|
||||
.from('profiles')
|
||||
.select('is_individual, individual_usage')
|
||||
.eq('id', session.user.id)
|
||||
.single();
|
||||
|
||||
if (senderProfileError) throw senderProfileError;
|
||||
|
||||
if (senderProfile && senderProfile.is_individual) {
|
||||
// Wenn der Sender ein Einzelnutzer ist, erhöhen wir seine Nutzung
|
||||
const { error: updateError } = await supabase
|
||||
.from('profiles')
|
||||
.update({
|
||||
individual_usage: senderProfile.individual_usage + amount,
|
||||
})
|
||||
.eq('id', session.user.id);
|
||||
|
||||
if (updateError) throw updateError;
|
||||
} else {
|
||||
// Wenn der Sender kein Einzelnutzer ist, müssen wir prüfen, ob er zu einem Team gehört
|
||||
// und die entsprechenden Kredite aktualisieren
|
||||
// Diese Logik würde komplexer sein und hängt von Ihrer spezifischen Implementierung ab
|
||||
}
|
||||
|
||||
Alert.alert('Erfolg', `${amount} Mana wurden erfolgreich an ${foundUser.email} gesendet.`);
|
||||
|
||||
// Formular zurücksetzen
|
||||
setRecipientEmail('');
|
||||
setManaAmount('');
|
||||
setDescription('');
|
||||
setFoundUser(null);
|
||||
|
||||
// Aktualisiere die Kredite des Benutzers
|
||||
fetchUserCredits(session.user.id);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Senden von Mana:', error);
|
||||
Alert.alert('Fehler', 'Es ist ein Fehler aufgetreten. Bitte versuchen Sie es später erneut.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Dynamische Stile basierend auf dem aktuellen Theme
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
|
|
@ -276,102 +69,21 @@ export default function SendMana() {
|
|||
color: isDarkMode ? '#93C5FD' : '#0055FF',
|
||||
marginLeft: 10,
|
||||
},
|
||||
formGroup: {
|
||||
marginBottom: 15,
|
||||
},
|
||||
label: {
|
||||
fontSize: 16,
|
||||
marginBottom: 8,
|
||||
fontWeight: '500',
|
||||
color: isDarkMode ? '#E5E7EB' : '#333',
|
||||
},
|
||||
input: {
|
||||
height: 50,
|
||||
borderWidth: 1,
|
||||
borderColor: isDarkMode ? '#4B5563' : '#ddd',
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 15,
|
||||
backgroundColor: isDarkMode ? '#374151' : '#f9f9f9',
|
||||
fontSize: 16,
|
||||
color: isDarkMode ? '#F9FAFB' : '#333',
|
||||
},
|
||||
searchContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
searchInput: {
|
||||
flex: 1,
|
||||
height: 50,
|
||||
borderWidth: 1,
|
||||
borderColor: isDarkMode ? '#4B5563' : '#ddd',
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 15,
|
||||
backgroundColor: isDarkMode ? '#374151' : '#f9f9f9',
|
||||
fontSize: 16,
|
||||
color: isDarkMode ? '#F9FAFB' : '#333',
|
||||
},
|
||||
searchButton: {
|
||||
backgroundColor: isDarkMode ? '#3B82F6' : '#0055FF',
|
||||
height: 50,
|
||||
paddingHorizontal: 15,
|
||||
borderRadius: 8,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginLeft: 10,
|
||||
},
|
||||
searchButtonText: {
|
||||
color: 'white',
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
userCard: {
|
||||
backgroundColor: isDarkMode ? '#1E3A8A' : '#f0f8ff',
|
||||
padding: 15,
|
||||
borderRadius: 8,
|
||||
marginBottom: 20,
|
||||
borderLeftWidth: 4,
|
||||
borderLeftColor: isDarkMode ? '#3B82F6' : '#0055FF',
|
||||
},
|
||||
userCardTitle: {
|
||||
fontSize: 14,
|
||||
color: isDarkMode ? '#9CA3AF' : '#666',
|
||||
marginBottom: 5,
|
||||
},
|
||||
userCardEmail: {
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
color: isDarkMode ? '#F9FAFB' : '#333',
|
||||
},
|
||||
userCardName: {
|
||||
fontSize: 14,
|
||||
color: isDarkMode ? '#9CA3AF' : '#666',
|
||||
marginTop: 5,
|
||||
},
|
||||
sendButton: {
|
||||
backgroundColor: isDarkMode ? '#3B82F6' : '#0055FF',
|
||||
height: 50,
|
||||
borderRadius: 8,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginTop: 10,
|
||||
},
|
||||
sendButtonText: {
|
||||
color: 'white',
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
disabledButton: {
|
||||
backgroundColor: isDarkMode ? '#4B5563' : '#ccc',
|
||||
},
|
||||
notLoggedIn: {
|
||||
fontSize: 16,
|
||||
color: isDarkMode ? '#9CA3AF' : '#666',
|
||||
textAlign: 'center',
|
||||
padding: 20,
|
||||
},
|
||||
comingSoon: {
|
||||
fontSize: 14,
|
||||
color: isDarkMode ? '#9CA3AF' : '#666',
|
||||
textAlign: 'center',
|
||||
fontStyle: 'italic',
|
||||
},
|
||||
});
|
||||
|
||||
if (!session) {
|
||||
if (!user) {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.notLoggedIn}>Bitte melden Sie sich an, um Mana zu senden.</Text>
|
||||
|
|
@ -388,79 +100,9 @@ export default function SendMana() {
|
|||
<Text style={styles.creditAmount}>{userCredits}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.formGroup}>
|
||||
<Text style={styles.label}>Empfänger E-Mail</Text>
|
||||
<View style={styles.searchContainer}>
|
||||
<TextInput
|
||||
style={styles.searchInput}
|
||||
value={recipientEmail}
|
||||
onChangeText={setRecipientEmail}
|
||||
placeholder="E-Mail des Empfängers"
|
||||
placeholderTextColor={isDarkMode ? '#9CA3AF' : '#9CA3AF'}
|
||||
autoCapitalize="none"
|
||||
keyboardType="email-address"
|
||||
/>
|
||||
<TouchableOpacity
|
||||
style={styles.searchButton}
|
||||
onPress={searchUser}
|
||||
disabled={searchLoading}
|
||||
>
|
||||
{searchLoading ? (
|
||||
<ActivityIndicator color="#fff" size="small" />
|
||||
) : (
|
||||
<Text style={styles.searchButtonText}>Suchen</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{foundUser && (
|
||||
<View style={styles.userCard}>
|
||||
<Text style={styles.userCardTitle}>Empfänger gefunden:</Text>
|
||||
<Text style={styles.userCardEmail}>{foundUser.email}</Text>
|
||||
{(foundUser.first_name || foundUser.last_name) && (
|
||||
<Text style={styles.userCardName}>
|
||||
{[foundUser.first_name, foundUser.last_name].filter(Boolean).join(' ')}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View style={styles.formGroup}>
|
||||
<Text style={styles.label}>Mana-Betrag</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={manaAmount}
|
||||
onChangeText={setManaAmount}
|
||||
placeholder="Betrag eingeben"
|
||||
placeholderTextColor={isDarkMode ? '#9CA3AF' : '#9CA3AF'}
|
||||
keyboardType="number-pad"
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.formGroup}>
|
||||
<Text style={styles.label}>Beschreibung (optional)</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={description}
|
||||
onChangeText={setDescription}
|
||||
placeholder="Grund für die Übertragung"
|
||||
placeholderTextColor={isDarkMode ? '#9CA3AF' : '#9CA3AF'}
|
||||
multiline
|
||||
/>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.sendButton, (!foundUser || loading) && styles.disabledButton]}
|
||||
onPress={sendMana}
|
||||
disabled={!foundUser || loading}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator color="#fff" size="small" />
|
||||
) : (
|
||||
<Text style={styles.sendButtonText}>Mana senden</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.comingSoon}>
|
||||
Die Mana-Übertragung zwischen Benutzern wird bald verfügbar sein.
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,257 +1,27 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
FlatList,
|
||||
TouchableOpacity,
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
import React from 'react';
|
||||
import { View, Text } from 'react-native';
|
||||
import { FontAwesome5 } from '@expo/vector-icons';
|
||||
import { supabase } from '../utils/supabase';
|
||||
import { Session } from '@supabase/supabase-js';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { useAuth } from '../context/AuthProvider';
|
||||
import { useTheme } from '../utils/themeContext';
|
||||
|
||||
interface Team {
|
||||
id: string;
|
||||
name: string;
|
||||
organization_id: string;
|
||||
organization_name?: string;
|
||||
allocated_credits?: number;
|
||||
used_credits?: number;
|
||||
created_at: string;
|
||||
}
|
||||
// TODO: Teams mit Credit-Zuweisung sind in mana-core-auth noch nicht implementiert.
|
||||
// Diese Komponente zeigt vorerst einen Hinweis an.
|
||||
|
||||
interface TeamListProps {
|
||||
hideTitle?: boolean;
|
||||
}
|
||||
|
||||
// Funktion zum Aktualisieren der Teamliste, die von außen aufgerufen werden kann
|
||||
export function refreshTeamList(userId: string, callback?: () => void) {
|
||||
if (!userId) return;
|
||||
|
||||
// Hole alle Teams, in denen der Benutzer Mitglied ist
|
||||
supabase
|
||||
.from('team_members')
|
||||
.select('team_id, allocated_credits, used_credits')
|
||||
.eq('user_id', userId)
|
||||
.then(({ data: teamMembers, error: teamMembersError }) => {
|
||||
if (teamMembersError) {
|
||||
console.error('Fehler beim Abrufen der Team-Mitglieder:', teamMembersError);
|
||||
if (callback) callback();
|
||||
return;
|
||||
}
|
||||
|
||||
if (teamMembers && teamMembers.length > 0) {
|
||||
// Extrahiere die Team-IDs
|
||||
const teamIds = teamMembers.map((member) => member.team_id);
|
||||
|
||||
// Hole die Team-Details
|
||||
supabase
|
||||
.from('teams')
|
||||
.select('id, name, organization_id, created_at')
|
||||
.in('id', teamIds)
|
||||
.then(({ data: teamsData, error: teamsError }) => {
|
||||
if (teamsError) {
|
||||
console.error('Fehler beim Abrufen der Teams:', teamsError);
|
||||
if (callback) callback();
|
||||
return;
|
||||
}
|
||||
|
||||
if (teamsData && teamsData.length > 0) {
|
||||
// Hole die Organisationsnamen für jedes Team
|
||||
const orgIds = [...new Set(teamsData.map((team) => team.organization_id))];
|
||||
|
||||
supabase
|
||||
.from('organizations')
|
||||
.select('id, name')
|
||||
.in('id', orgIds)
|
||||
.then(({ data: orgsData, error: orgsError }) => {
|
||||
if (orgsError) {
|
||||
console.error('Fehler beim Abrufen der Organisationen:', orgsError);
|
||||
if (callback) callback();
|
||||
return;
|
||||
}
|
||||
|
||||
// Globales Event auslösen, um alle TeamList-Komponenten zu aktualisieren
|
||||
const event = new CustomEvent('teamlist-refresh', {
|
||||
detail: {
|
||||
teams: teamsData.map((team) => {
|
||||
const org = orgsData?.find((org) => org.id === team.organization_id);
|
||||
const memberInfo = teamMembers.find((member) => member.team_id === team.id);
|
||||
|
||||
return {
|
||||
...team,
|
||||
organization_name: org?.name || 'Unbekannte Organisation',
|
||||
allocated_credits: memberInfo?.allocated_credits || 0,
|
||||
used_credits: memberInfo?.used_credits || 0,
|
||||
};
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
// Event auslösen
|
||||
if (typeof window !== 'undefined') {
|
||||
window.dispatchEvent(event);
|
||||
}
|
||||
|
||||
if (callback) callback();
|
||||
});
|
||||
} else {
|
||||
// Globales Event mit leerer Liste auslösen
|
||||
if (typeof window !== 'undefined') {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('teamlist-refresh', { detail: { teams: [] } })
|
||||
);
|
||||
}
|
||||
if (callback) callback();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Globales Event mit leerer Liste auslösen
|
||||
if (typeof window !== 'undefined') {
|
||||
window.dispatchEvent(new CustomEvent('teamlist-refresh', { detail: { teams: [] } }));
|
||||
}
|
||||
if (callback) callback();
|
||||
}
|
||||
});
|
||||
// Stub for external callers that used refreshTeamList
|
||||
export function refreshTeamList(_userId: string, callback?: () => void) {
|
||||
// No-op: Teams are not yet available via mana-core-auth
|
||||
if (callback) callback();
|
||||
}
|
||||
|
||||
export default function TeamList({ hideTitle = false }: TeamListProps) {
|
||||
const router = useRouter();
|
||||
const [session, setSession] = useState<Session | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [teams, setTeams] = useState<Team[]>([]);
|
||||
const { isDarkMode } = useTheme();
|
||||
const { user } = useAuth();
|
||||
|
||||
useEffect(() => {
|
||||
// Prüfe den aktuellen Authentifizierungsstatus
|
||||
supabase.auth.getSession().then(({ data: { session } }) => {
|
||||
setSession(session);
|
||||
if (session) {
|
||||
fetchUserTeams(session.user.id);
|
||||
} else {
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
// Abonniere Authentifizierungsänderungen
|
||||
const {
|
||||
data: { subscription },
|
||||
} = supabase.auth.onAuthStateChange((_event, session) => {
|
||||
setSession(session);
|
||||
if (session) {
|
||||
fetchUserTeams(session.user.id);
|
||||
} else {
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
// Listener für teamlist-refresh Event hinzufügen
|
||||
const handleTeamListRefresh = (event: any) => {
|
||||
if (event.detail && event.detail.teams) {
|
||||
setTeams(event.detail.teams);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('teamlist-refresh', handleTeamListRefresh);
|
||||
}
|
||||
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
if (typeof window !== 'undefined') {
|
||||
window.removeEventListener('teamlist-refresh', handleTeamListRefresh);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
async function fetchUserTeams(userId: string) {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// Hole alle Teams, in denen der Benutzer Mitglied ist
|
||||
const { data: teamMembers, error: teamMembersError } = await supabase
|
||||
.from('team_members')
|
||||
.select('team_id, allocated_credits, used_credits')
|
||||
.eq('user_id', userId);
|
||||
|
||||
if (teamMembersError) throw teamMembersError;
|
||||
|
||||
if (teamMembers && teamMembers.length > 0) {
|
||||
// Extrahiere die Team-IDs
|
||||
const teamIds = teamMembers.map((member) => member.team_id);
|
||||
|
||||
// Hole die Team-Details
|
||||
const { data: teamsData, error: teamsError } = await supabase
|
||||
.from('teams')
|
||||
.select('id, name, organization_id, created_at')
|
||||
.in('id', teamIds);
|
||||
|
||||
if (teamsError) throw teamsError;
|
||||
|
||||
if (teamsData && teamsData.length > 0) {
|
||||
// Hole die Organisationsnamen für jedes Team
|
||||
const orgIds = [...new Set(teamsData.map((team) => team.organization_id))];
|
||||
|
||||
const { data: orgsData, error: orgsError } = await supabase
|
||||
.from('organizations')
|
||||
.select('id, name')
|
||||
.in('id', orgIds);
|
||||
|
||||
if (orgsError) throw orgsError;
|
||||
|
||||
// Füge Organisationsnamen und Kredit-Informationen zu den Teams hinzu
|
||||
const enhancedTeams = teamsData.map((team) => {
|
||||
const org = orgsData?.find((org) => org.id === team.organization_id);
|
||||
const memberInfo = teamMembers.find((member) => member.team_id === team.id);
|
||||
|
||||
return {
|
||||
...team,
|
||||
organization_name: org?.name || 'Unbekannte Organisation',
|
||||
allocated_credits: memberInfo?.allocated_credits || 0,
|
||||
used_credits: memberInfo?.used_credits || 0,
|
||||
};
|
||||
});
|
||||
|
||||
setTeams(enhancedTeams);
|
||||
} else {
|
||||
setTeams([]);
|
||||
}
|
||||
} else {
|
||||
setTeams([]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Abrufen der Teams:', error);
|
||||
Alert.alert('Fehler', 'Es ist ein Fehler beim Laden Ihrer Teams aufgetreten.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Keine Aufklappfunktion mehr benötigt
|
||||
|
||||
const navigateToTeamDetails = (teamId: string, teamName: string) => {
|
||||
// Verwende die korrekte Expo Router Navigation
|
||||
router.push({
|
||||
pathname: '/teams/[id]',
|
||||
params: { id: teamId, name: teamName },
|
||||
});
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('de-DE', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
});
|
||||
};
|
||||
|
||||
if (!session) {
|
||||
if (!user) {
|
||||
return (
|
||||
<Text
|
||||
className={`text-base ${isDarkMode ? 'text-gray-300' : 'text-gray-600'} mx-2.5 p-5 text-center`}
|
||||
|
|
@ -261,43 +31,6 @@ export default function TeamList({ hideTitle = false }: TeamListProps) {
|
|||
);
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<View className="mx-2.5 flex-1 items-center justify-center p-4">
|
||||
<ActivityIndicator size="large" color="#0055FF" />
|
||||
<Text
|
||||
className={`text-base ${isDarkMode ? 'text-gray-300' : 'text-gray-600'} mt-2.5 text-center`}
|
||||
>
|
||||
Lade Teams...
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (teams.length === 0) {
|
||||
return (
|
||||
<View className="mx-2.5 flex-1 items-center justify-center p-4">
|
||||
<FontAwesome5
|
||||
name="users-slash"
|
||||
size={50}
|
||||
color={isDarkMode ? '#4B5563' : '#ccc'}
|
||||
className="mb-4"
|
||||
/>
|
||||
<Text
|
||||
className={`text-lg font-medium ${isDarkMode ? 'text-gray-100' : 'text-gray-800'} mb-2.5 text-center`}
|
||||
>
|
||||
Sie sind derzeit kein Mitglied in einem Team.
|
||||
</Text>
|
||||
<Text
|
||||
className={`text-sm ${isDarkMode ? 'text-gray-300' : 'text-gray-600'} px-5 text-center`}
|
||||
>
|
||||
Erstellen Sie ein neues Team oder bitten Sie einen Administrator, Sie einem Team
|
||||
hinzuzufügen.
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{!hideTitle && (
|
||||
|
|
@ -307,109 +40,24 @@ export default function TeamList({ hideTitle = false }: TeamListProps) {
|
|||
Meine Teams
|
||||
</Text>
|
||||
)}
|
||||
<FlatList
|
||||
data={teams}
|
||||
keyExtractor={(item) => item.id}
|
||||
renderItem={({ item }) => (
|
||||
<TouchableOpacity
|
||||
className={`${isDarkMode ? 'bg-gray-700' : 'bg-gray-50'} mb-2.5 rounded-lg p-4 shadow-sm`}
|
||||
style={Platform.OS === 'web' ? { cursor: 'pointer' } : undefined}
|
||||
onPress={() => navigateToTeamDetails(item.id, item.name)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
{/* Header mit Teamname */}
|
||||
<View className="mb-3 flex-row items-center justify-between">
|
||||
<View className="flex-row items-center">
|
||||
<FontAwesome5
|
||||
name="users"
|
||||
size={18}
|
||||
color={isDarkMode ? '#93C5FD' : '#0055FF'}
|
||||
className="mr-2.5"
|
||||
/>
|
||||
<Text
|
||||
className={`text-lg font-semibold ${isDarkMode ? 'text-gray-100' : 'text-gray-800'}`}
|
||||
>
|
||||
{item.name}
|
||||
</Text>
|
||||
</View>
|
||||
<FontAwesome5
|
||||
name="chevron-right"
|
||||
size={14}
|
||||
color={isDarkMode ? '#93C5FD' : '#0055FF'}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Mittlerer Bereich mit Organisation und Erstellungsdatum */}
|
||||
<View className="mb-3 flex-row items-center justify-between">
|
||||
<View className="flex-row items-center">
|
||||
<FontAwesome5
|
||||
name="building"
|
||||
size={14}
|
||||
color={isDarkMode ? '#9CA3AF' : '#666'}
|
||||
className="mr-2"
|
||||
/>
|
||||
<Text className={`text-sm ${isDarkMode ? 'text-gray-300' : 'text-gray-600'}`}>
|
||||
{item.organization_name}
|
||||
</Text>
|
||||
</View>
|
||||
<View className="flex-row items-center">
|
||||
<FontAwesome5
|
||||
name="calendar-alt"
|
||||
size={14}
|
||||
color={isDarkMode ? '#9CA3AF' : '#666'}
|
||||
className="mr-2"
|
||||
/>
|
||||
<Text className={`text-sm ${isDarkMode ? 'text-gray-300' : 'text-gray-600'}`}>
|
||||
{formatDate(item.created_at)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Unterer Bereich mit Kreditinformationen */}
|
||||
<View
|
||||
className={`mt-1 flex-row justify-between border-t pt-2 ${isDarkMode ? 'border-gray-600' : 'border-gray-200'}`}
|
||||
>
|
||||
<View className="flex-1 items-center">
|
||||
<Text className={`text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-600'} mb-1`}>
|
||||
Zugewiesen
|
||||
</Text>
|
||||
<Text
|
||||
className={`text-base font-semibold ${isDarkMode ? 'text-gray-100' : 'text-gray-800'}`}
|
||||
>
|
||||
{item.allocated_credits}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View
|
||||
className={`flex-1 items-center border-x ${isDarkMode ? 'border-gray-600' : 'border-gray-200'}`}
|
||||
>
|
||||
<Text className={`text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-600'} mb-1`}>
|
||||
Verwendet
|
||||
</Text>
|
||||
<Text
|
||||
className={`text-base font-semibold ${isDarkMode ? 'text-gray-100' : 'text-gray-800'}`}
|
||||
>
|
||||
{item.used_credits}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View className="flex-1 items-center">
|
||||
<Text className={`text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-600'} mb-1`}>
|
||||
Verfügbar
|
||||
</Text>
|
||||
<Text
|
||||
className={`text-base font-semibold ${(item.allocated_credits || 0) - (item.used_credits || 0) > 0 ? (isDarkMode ? 'text-green-400' : 'text-green-600') : isDarkMode ? 'text-red-400' : 'text-red-600'}`}
|
||||
>
|
||||
{(item.allocated_credits || 0) - (item.used_credits || 0)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
className="px-2.5 pb-5"
|
||||
/>
|
||||
<View className="mx-2.5 flex-1 items-center justify-center p-4">
|
||||
<FontAwesome5
|
||||
name="users"
|
||||
size={50}
|
||||
color={isDarkMode ? '#4B5563' : '#ccc'}
|
||||
className="mb-4"
|
||||
/>
|
||||
<Text
|
||||
className={`text-lg font-medium ${isDarkMode ? 'text-gray-100' : 'text-gray-800'} mb-2.5 text-center`}
|
||||
>
|
||||
Teams werden bald verfügbar sein.
|
||||
</Text>
|
||||
<Text
|
||||
className={`text-sm ${isDarkMode ? 'text-gray-300' : 'text-gray-600'} px-5 text-center`}
|
||||
>
|
||||
Die Team-Verwaltung mit Credit-Zuweisung wird derzeit auf das neue Auth-System migriert.
|
||||
</Text>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// NativeWind wird für das Styling verwendet, daher sind keine StyleSheet-Definitionen erforderlich
|
||||
|
|
|
|||
|
|
@ -1,464 +1,21 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
FlatList,
|
||||
TouchableOpacity,
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
TextInput,
|
||||
} from 'react-native';
|
||||
import React from 'react';
|
||||
import { View, Text } from 'react-native';
|
||||
import { FontAwesome5 } from '@expo/vector-icons';
|
||||
import { supabase } from '../utils/supabase';
|
||||
import { Session } from '@supabase/supabase-js';
|
||||
import { useAuth } from '../context/AuthProvider';
|
||||
import { useTheme } from '../utils/themeContext';
|
||||
|
||||
interface TeamMember {
|
||||
// Anpassung an die tatsächliche Datenbankstruktur
|
||||
user_id: string;
|
||||
team_id: string;
|
||||
allocated_credits: number;
|
||||
used_credits: number;
|
||||
first_name?: string;
|
||||
last_name?: string;
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
interface TeamDetails {
|
||||
id: string;
|
||||
name: string;
|
||||
organization_id: string;
|
||||
organization_name?: string;
|
||||
created_at: string;
|
||||
}
|
||||
// TODO: Team-Mitgliederverwaltung mit Credit-Zuweisung ist in mana-core-auth noch nicht implementiert.
|
||||
// Diese Komponente zeigt vorerst einen Hinweis an.
|
||||
|
||||
interface TeamMembersProps {
|
||||
teamId: string;
|
||||
}
|
||||
|
||||
export default function TeamMembers({ teamId }: TeamMembersProps) {
|
||||
const [session, setSession] = useState<Session | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [teamDetails, setTeamDetails] = useState<TeamDetails | null>(null);
|
||||
const [members, setMembers] = useState<TeamMember[]>([]);
|
||||
const [newMemberEmail, setNewMemberEmail] = useState('');
|
||||
const [inviting, setInviting] = useState(false);
|
||||
const [_userRole, setUserRole] = useState<string | null>(null);
|
||||
const [isAdmin, setIsAdmin] = useState(false);
|
||||
export default function TeamMembers({ teamId: _teamId }: TeamMembersProps) {
|
||||
const { user } = useAuth();
|
||||
const { isDarkMode } = useTheme();
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [newTeamName, setNewTeamName] = useState('');
|
||||
const [updatingName, setUpdatingName] = useState(false);
|
||||
|
||||
// Zustandsvariablen für die Bearbeitung der Mitgliederlimits
|
||||
const [editingMemberId, setEditingMemberId] = useState<string | null>(null);
|
||||
const [newCreditLimit, setNewCreditLimit] = useState('');
|
||||
const [updatingLimit, setUpdatingLimit] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!teamId) {
|
||||
Alert.alert('Fehler', 'Team-ID nicht gefunden');
|
||||
return;
|
||||
}
|
||||
|
||||
// Setze den neuen Teamnamen, wenn sich die Teamdetails ändern
|
||||
if (teamDetails) {
|
||||
setNewTeamName(teamDetails.name);
|
||||
}
|
||||
|
||||
// Prüfe den aktuellen Authentifizierungsstatus
|
||||
supabase.auth.getSession().then(({ data: { session } }) => {
|
||||
setSession(session);
|
||||
if (session) {
|
||||
fetchTeamDetails(teamId);
|
||||
fetchTeamMembers(teamId);
|
||||
checkUserRole(session.user.id, teamId);
|
||||
} else {
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
// Abonniere Authentifizierungsänderungen
|
||||
const {
|
||||
data: { subscription },
|
||||
} = supabase.auth.onAuthStateChange((_event, session) => {
|
||||
setSession(session);
|
||||
if (session) {
|
||||
fetchTeamDetails(teamId);
|
||||
fetchTeamMembers(teamId);
|
||||
checkUserRole(session.user.id, teamId);
|
||||
} else {
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
return () => subscription.unsubscribe();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [teamId]);
|
||||
|
||||
async function checkUserRole(userId: string, teamId: string) {
|
||||
try {
|
||||
// Vereinfachte Version: Wir setzen isAdmin auf true, damit die Optionen zum Hinzufügen und Entfernen
|
||||
// von Teammitgliedern immer angezeigt werden
|
||||
setIsAdmin(true);
|
||||
setUserRole('admin');
|
||||
|
||||
// Prüfe, ob der Benutzer ein Teammitglied ist
|
||||
const { data: memberData, error: memberError } = await supabase
|
||||
.from('team_members')
|
||||
.select('user_id, team_id')
|
||||
.eq('user_id', userId)
|
||||
.eq('team_id', teamId);
|
||||
|
||||
if (memberError) {
|
||||
console.error('Fehler beim Prüfen der Mitgliedschaft:', memberError);
|
||||
}
|
||||
|
||||
// Wenn der Benutzer kein Mitglied ist, setzen wir die Rolle auf null
|
||||
if (!memberData || memberData.length === 0) {
|
||||
setUserRole(null);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Prüfen der Benutzerrolle:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchTeamDetails(teamId: string) {
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('teams')
|
||||
.select('id, name, organization_id, created_at')
|
||||
.eq('id', teamId)
|
||||
.single();
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
if (data) {
|
||||
// Hole den Organisationsnamen
|
||||
const { data: orgData, error: orgError } = await supabase
|
||||
.from('organizations')
|
||||
.select('name')
|
||||
.eq('id', data.organization_id)
|
||||
.single();
|
||||
|
||||
if (orgError) throw orgError;
|
||||
|
||||
setTeamDetails({
|
||||
...data,
|
||||
organization_name: orgData?.name || 'Unbekannte Organisation',
|
||||
});
|
||||
|
||||
// Setze den neuen Teamnamen
|
||||
setNewTeamName(data.name);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Abrufen der Team-Details:', error);
|
||||
Alert.alert('Fehler', 'Es ist ein Fehler beim Laden der Team-Details aufgetreten.');
|
||||
}
|
||||
}
|
||||
|
||||
const startEditing = () => {
|
||||
setIsEditing(true);
|
||||
if (teamDetails) {
|
||||
setNewTeamName(teamDetails.name);
|
||||
}
|
||||
};
|
||||
|
||||
const cancelEditing = () => {
|
||||
setIsEditing(false);
|
||||
if (teamDetails) {
|
||||
setNewTeamName(teamDetails.name);
|
||||
}
|
||||
};
|
||||
|
||||
const updateTeamName = async () => {
|
||||
if (!newTeamName.trim()) {
|
||||
Alert.alert('Fehler', 'Der Teamname darf nicht leer sein.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (teamDetails && newTeamName.trim() === teamDetails.name) {
|
||||
setIsEditing(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setUpdatingName(true);
|
||||
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from('teams')
|
||||
.update({ name: newTeamName.trim() })
|
||||
.eq('id', teamId);
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
// Aktualisiere die Teamdetails
|
||||
if (teamDetails) {
|
||||
setTeamDetails({
|
||||
...teamDetails,
|
||||
name: newTeamName.trim(),
|
||||
});
|
||||
}
|
||||
|
||||
setIsEditing(false);
|
||||
Alert.alert('Erfolg', 'Der Teamname wurde erfolgreich aktualisiert.');
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Aktualisieren des Teamnamens:', error);
|
||||
Alert.alert('Fehler', 'Es ist ein Fehler beim Aktualisieren des Teamnamens aufgetreten.');
|
||||
} finally {
|
||||
setUpdatingName(false);
|
||||
}
|
||||
};
|
||||
|
||||
async function fetchTeamMembers(teamId: string) {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// Hole alle Teammitglieder basierend auf der tatsächlichen Datenbankstruktur
|
||||
const { data: memberData, error: memberError } = await supabase
|
||||
.from('team_members')
|
||||
.select('user_id, team_id, allocated_credits, used_credits, created_at, updated_at')
|
||||
.eq('team_id', teamId);
|
||||
|
||||
if (memberError) throw memberError;
|
||||
|
||||
if (memberData && memberData.length > 0) {
|
||||
// Hole die Benutzerprofile für jedes Mitglied
|
||||
const userIds = memberData.map((member) => member.user_id);
|
||||
|
||||
// Abfrage der Profilinformationen aus der profiles-Tabelle
|
||||
const { data: userData, error: userError } = await supabase
|
||||
.from('profiles')
|
||||
.select('id, first_name, last_name')
|
||||
.in('id', userIds);
|
||||
|
||||
if (userError) throw userError;
|
||||
|
||||
// Füge Benutzerinformationen zu den Mitgliedern hinzu
|
||||
const enhancedMembers = memberData.map((member) => {
|
||||
const user = userData?.find((u) => u.id === member.user_id);
|
||||
return {
|
||||
...member,
|
||||
first_name: user?.first_name,
|
||||
last_name: user?.last_name,
|
||||
};
|
||||
});
|
||||
|
||||
setMembers(enhancedMembers as TeamMember[]);
|
||||
} else {
|
||||
setMembers([]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Abrufen der Teammitglieder:', error);
|
||||
Alert.alert('Fehler', 'Es ist ein Fehler beim Laden der Teammitglieder aufgetreten.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function inviteMember() {
|
||||
if (!newMemberEmail || !newMemberEmail.includes('@')) {
|
||||
Alert.alert('Fehler', 'Bitte geben Sie eine gültige E-Mail-Adresse ein.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!teamId || !isAdmin) {
|
||||
Alert.alert('Fehler', 'Sie haben keine Berechtigung, Mitglieder einzuladen.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setInviting(true);
|
||||
|
||||
// Suche nach dem Benutzer mit dieser E-Mail
|
||||
const { data: profiles, error: profilesError } = await supabase
|
||||
.from('profiles')
|
||||
.select('id, email')
|
||||
.ilike('email', newMemberEmail.trim().toLowerCase());
|
||||
|
||||
if (profilesError) {
|
||||
console.error('Fehler beim Suchen des Benutzers:', profilesError);
|
||||
Alert.alert('Fehler', 'Benutzer konnte nicht gefunden werden.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!profiles || profiles.length === 0) {
|
||||
// Wenn der Benutzer nicht gefunden wurde, zeigen wir eine Meldung an
|
||||
Alert.alert(
|
||||
'Benutzer nicht gefunden',
|
||||
'Möchten Sie eine Einladung an diese E-Mail-Adresse senden?',
|
||||
[
|
||||
{
|
||||
text: 'Abbrechen',
|
||||
style: 'cancel',
|
||||
},
|
||||
{
|
||||
text: 'Einladen',
|
||||
onPress: () => {
|
||||
// Hier könnte eine Einladungs-E-Mail gesendet werden
|
||||
// Für jetzt zeigen wir nur eine Erfolgsmeldung an
|
||||
Alert.alert('Erfolg', `Eine Einladung wurde an ${newMemberEmail} gesendet.`);
|
||||
setNewMemberEmail('');
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const userId = profiles[0].id;
|
||||
|
||||
// Prüfe, ob der Benutzer bereits Mitglied ist
|
||||
const { data: existingMember, error: existingError } = await supabase
|
||||
.from('team_members')
|
||||
.select('user_id, team_id')
|
||||
.eq('user_id', userId)
|
||||
.eq('team_id', teamId);
|
||||
|
||||
if (existingError) throw existingError;
|
||||
|
||||
if (existingMember && existingMember.length > 0) {
|
||||
Alert.alert('Information', 'Dieser Benutzer ist bereits Mitglied des Teams.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Füge den Benutzer zum Team hinzu
|
||||
const { error: addError } = await supabase.from('team_members').insert([
|
||||
{
|
||||
user_id: userId,
|
||||
team_id: teamId,
|
||||
allocated_credits: 0,
|
||||
used_credits: 0,
|
||||
},
|
||||
]);
|
||||
|
||||
if (addError) throw addError;
|
||||
|
||||
Alert.alert('Erfolg', 'Benutzer wurde erfolgreich zum Team hinzugefügt.');
|
||||
setNewMemberEmail('');
|
||||
fetchTeamMembers(teamId);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Einladen des Benutzers:', error);
|
||||
Alert.alert('Fehler', 'Es ist ein Fehler beim Einladen des Benutzers aufgetreten.');
|
||||
} finally {
|
||||
setInviting(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Direktes Entfernen ohne Alert-Dialog
|
||||
async function removeMember(userId: string, memberName: string) {
|
||||
if (!isAdmin) {
|
||||
Alert.alert('Fehler', 'Sie haben keine Berechtigung, Mitglieder zu entfernen.');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Versuche, Mitglied zu entfernen: ${memberName} (${userId}) aus Team ${teamId}`);
|
||||
|
||||
try {
|
||||
console.log('Sende Löschanfrage an Supabase...');
|
||||
console.log(`Parameter: user_id=${userId}, team_id=${teamId}`);
|
||||
|
||||
// Beachte, dass team_members einen zusammengesetzten Primärschlüssel aus team_id und user_id hat
|
||||
const { data, error } = await supabase
|
||||
.from('team_members')
|
||||
.delete()
|
||||
.eq('user_id', userId)
|
||||
.eq('team_id', teamId)
|
||||
.select();
|
||||
|
||||
if (error) {
|
||||
console.error('Supabase-Fehler beim Entfernen:', error);
|
||||
Alert.alert('Fehler', `Fehler beim Entfernen des Mitglieds: ${error.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Löschantwort von Supabase:', data);
|
||||
Alert.alert('Erfolg', 'Mitglied wurde erfolgreich aus dem Team entfernt.');
|
||||
|
||||
// Aktualisiere die Mitgliederliste
|
||||
fetchTeamMembers(teamId);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Entfernen des Mitglieds:', error);
|
||||
Alert.alert(
|
||||
'Fehler',
|
||||
'Es ist ein Fehler beim Entfernen des Mitglieds aufgetreten. Bitte überprüfen Sie die Konsole für weitere Details.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Funktion zum Starten der Bearbeitung des Credit-Limits für ein Mitglied
|
||||
const startEditingLimit = (userId: string, currentLimit: number) => {
|
||||
if (!isAdmin) {
|
||||
Alert.alert('Fehler', 'Sie haben keine Berechtigung, Mitgliederlimits zu bearbeiten.');
|
||||
return;
|
||||
}
|
||||
|
||||
setEditingMemberId(userId);
|
||||
setNewCreditLimit(currentLimit.toString());
|
||||
};
|
||||
|
||||
// Funktion zum Abbrechen der Bearbeitung
|
||||
const cancelEditingLimit = () => {
|
||||
setEditingMemberId(null);
|
||||
setNewCreditLimit('');
|
||||
};
|
||||
|
||||
// Funktion zum Aktualisieren des Credit-Limits eines Mitglieds
|
||||
const updateMemberLimit = async () => {
|
||||
if (!editingMemberId || !teamId) {
|
||||
Alert.alert('Fehler', 'Mitglied oder Team-ID nicht gefunden.');
|
||||
return;
|
||||
}
|
||||
|
||||
const creditLimit = parseInt(newCreditLimit);
|
||||
if (isNaN(creditLimit) || creditLimit < 0) {
|
||||
Alert.alert('Fehler', 'Bitte geben Sie einen gültigen Wert für das Credit-Limit ein.');
|
||||
return;
|
||||
}
|
||||
|
||||
setUpdatingLimit(true);
|
||||
|
||||
try {
|
||||
// Aktualisiere das Credit-Limit in der Datenbank
|
||||
const { error } = await supabase
|
||||
.from('team_members')
|
||||
.update({ allocated_credits: creditLimit })
|
||||
.eq('user_id', editingMemberId)
|
||||
.eq('team_id', teamId);
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
// Aktualisiere die lokale Mitgliederliste
|
||||
setMembers(
|
||||
members.map((member) => {
|
||||
if (member.user_id === editingMemberId) {
|
||||
return { ...member, allocated_credits: creditLimit };
|
||||
}
|
||||
return member;
|
||||
})
|
||||
);
|
||||
|
||||
Alert.alert('Erfolg', 'Das Credit-Limit wurde erfolgreich aktualisiert.');
|
||||
setEditingMemberId(null);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Aktualisieren des Credit-Limits:', error);
|
||||
Alert.alert('Fehler', 'Es ist ein Fehler beim Aktualisieren des Credit-Limits aufgetreten.');
|
||||
} finally {
|
||||
setUpdatingLimit(false);
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('de-DE', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
});
|
||||
};
|
||||
|
||||
if (!session) {
|
||||
if (!user) {
|
||||
return (
|
||||
<View
|
||||
className={`flex-1 ${isDarkMode ? 'bg-gray-800' : 'bg-white'} m-2.5 rounded-lg p-4 shadow`}
|
||||
|
|
@ -472,106 +29,8 @@ export default function TeamMembers({ teamId }: TeamMembersProps) {
|
|||
);
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<View
|
||||
className={`flex-1 ${isDarkMode ? 'bg-gray-800' : 'bg-white'} m-2.5 rounded-lg p-4 shadow`}
|
||||
>
|
||||
<ActivityIndicator size="large" color="#0055FF" />
|
||||
<Text
|
||||
className={`text-base ${isDarkMode ? 'text-gray-300' : 'text-gray-600'} mt-2.5 text-center`}
|
||||
>
|
||||
Lade Teammitglieder...
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="flex-1">
|
||||
{teamDetails && (
|
||||
<View className={`${isDarkMode ? 'bg-gray-800' : 'bg-white'} m-2.5 rounded-lg p-4 shadow`}>
|
||||
{isEditing ? (
|
||||
<View>
|
||||
<View className="mb-3 flex-row items-center">
|
||||
<Text
|
||||
className={`text-lg font-bold ${isDarkMode ? 'text-gray-100' : 'text-gray-800'} mr-2`}
|
||||
>
|
||||
Teamname bearbeiten:
|
||||
</Text>
|
||||
</View>
|
||||
<View className="flex-row items-center">
|
||||
<TextInput
|
||||
className={`flex-1 ${isDarkMode ? 'border-gray-600 bg-gray-700 text-white' : 'border-gray-300 bg-gray-50 text-gray-800'} mr-2 rounded-lg border px-3 py-2`}
|
||||
value={newTeamName}
|
||||
onChangeText={setNewTeamName}
|
||||
placeholder="Teamname"
|
||||
placeholderTextColor={isDarkMode ? '#9CA3AF' : '#6B7280'}
|
||||
/>
|
||||
</View>
|
||||
<View className="mb-2 mt-3 flex-row justify-end">
|
||||
<TouchableOpacity
|
||||
className={`mr-2 flex-row items-center rounded-lg px-4 py-2 ${isDarkMode ? 'bg-gray-700' : 'bg-gray-200'}`}
|
||||
onPress={cancelEditing}
|
||||
disabled={updatingName}
|
||||
>
|
||||
<Text
|
||||
className={`${isDarkMode ? 'text-gray-300' : 'text-gray-700'} font-semibold`}
|
||||
>
|
||||
Abbrechen
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
className={`flex-row items-center rounded-lg px-4 py-2 ${isDarkMode ? 'bg-blue-700' : 'bg-blue-600'}`}
|
||||
onPress={updateTeamName}
|
||||
disabled={updatingName}
|
||||
>
|
||||
{updatingName ? (
|
||||
<Text className="font-semibold text-white">Speichern...</Text>
|
||||
) : (
|
||||
<Text className="font-semibold text-white">Speichern</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<Text className={`text-sm ${isDarkMode ? 'text-gray-300' : 'text-gray-600'} mb-3`}>
|
||||
Organisation: {teamDetails.organization_name}
|
||||
</Text>
|
||||
<Text className={`text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`}>
|
||||
Erstellt am: {formatDate(teamDetails.created_at)}
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
<View>
|
||||
<View className="mb-1 flex-row items-center justify-between">
|
||||
<Text
|
||||
className={`text-xl font-bold ${isDarkMode ? 'text-gray-100' : 'text-gray-800'}`}
|
||||
>
|
||||
{teamDetails.name}
|
||||
</Text>
|
||||
{isAdmin && (
|
||||
<TouchableOpacity
|
||||
className={`rounded-full p-2 ${isDarkMode ? 'bg-gray-700' : 'bg-gray-100'}`}
|
||||
onPress={startEditing}
|
||||
>
|
||||
<FontAwesome5
|
||||
name="edit"
|
||||
size={16}
|
||||
color={isDarkMode ? '#93C5FD' : '#0055FF'}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
<Text className={`text-sm ${isDarkMode ? 'text-gray-300' : 'text-gray-600'} mb-3`}>
|
||||
Organisation: {teamDetails.organization_name}
|
||||
</Text>
|
||||
<Text className={`text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`}>
|
||||
Erstellt am: {formatDate(teamDetails.created_at)}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View className={`${isDarkMode ? 'bg-gray-800' : 'bg-white'} m-2.5 rounded-lg p-4 shadow`}>
|
||||
<Text
|
||||
className={`text-lg font-semibold ${isDarkMode ? 'text-gray-100' : 'text-gray-800'} mb-4`}
|
||||
|
|
@ -579,163 +38,15 @@ export default function TeamMembers({ teamId }: TeamMembersProps) {
|
|||
Teammitglieder
|
||||
</Text>
|
||||
|
||||
{isAdmin && (
|
||||
<View className={`mb-4 ${isDarkMode ? 'bg-gray-700' : 'bg-gray-50'} rounded-lg p-3`}>
|
||||
<Text
|
||||
className={`text-sm font-medium ${isDarkMode ? 'text-gray-200' : 'text-gray-700'} mb-2`}
|
||||
>
|
||||
Neues Mitglied einladen
|
||||
</Text>
|
||||
<View className="flex-row">
|
||||
<TextInput
|
||||
className={`flex-1 ${isDarkMode ? 'border-gray-500 bg-gray-600 text-white' : 'border-gray-300 bg-white text-gray-800'} mr-2 rounded-lg border px-3 py-2`}
|
||||
placeholder="E-Mail-Adresse"
|
||||
placeholderTextColor={isDarkMode ? '#9CA3AF' : '#6B7280'}
|
||||
value={newMemberEmail}
|
||||
onChangeText={setNewMemberEmail}
|
||||
autoCapitalize="none"
|
||||
keyboardType="email-address"
|
||||
/>
|
||||
<TouchableOpacity
|
||||
className="items-center justify-center rounded-lg bg-blue-600 px-4 py-2"
|
||||
onPress={inviteMember}
|
||||
disabled={inviting}
|
||||
>
|
||||
{inviting ? (
|
||||
<ActivityIndicator size="small" color="white" />
|
||||
) : (
|
||||
<Text className="font-medium text-white">Einladen</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{members.length === 0 ? (
|
||||
<View className="items-center justify-center py-8">
|
||||
<FontAwesome5 name="users-slash" size={50} color={isDarkMode ? '#4B5563' : '#ccc'} />
|
||||
<Text
|
||||
className={`text-base ${isDarkMode ? 'text-gray-300' : 'text-gray-600'} mt-4 text-center`}
|
||||
>
|
||||
Keine Mitglieder in diesem Team gefunden.
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
<FlatList
|
||||
data={members}
|
||||
keyExtractor={(item) => item.user_id}
|
||||
renderItem={({ item }) => (
|
||||
<View
|
||||
className={`flex-row items-center justify-between ${isDarkMode ? 'bg-gray-700' : 'bg-gray-50'} mb-2 rounded-lg p-3`}
|
||||
>
|
||||
<View className="flex-1">
|
||||
<Text
|
||||
className={`text-base font-medium ${isDarkMode ? 'text-gray-100' : 'text-gray-800'}`}
|
||||
>
|
||||
{item.first_name && item.last_name
|
||||
? `${item.first_name} ${item.last_name}`
|
||||
: `Benutzer ${item.user_id.substring(0, 8)}...`}
|
||||
</Text>
|
||||
<View className="mt-1 flex-row items-center">
|
||||
{editingMemberId === item.user_id ? (
|
||||
<View className="mr-4 flex-row items-center">
|
||||
<Text
|
||||
className={`text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-500'} mr-2`}
|
||||
>
|
||||
Credits:
|
||||
</Text>
|
||||
<TextInput
|
||||
className={`${isDarkMode ? 'border-gray-500 bg-gray-600 text-white' : 'border-gray-300 bg-white text-gray-800'} w-16 rounded border px-2 py-1 text-xs`}
|
||||
value={newCreditLimit}
|
||||
onChangeText={setNewCreditLimit}
|
||||
keyboardType="numeric"
|
||||
autoFocus
|
||||
/>
|
||||
<TouchableOpacity
|
||||
onPress={updateMemberLimit}
|
||||
className="ml-2 p-1"
|
||||
disabled={updatingLimit}
|
||||
>
|
||||
<FontAwesome5
|
||||
name="check"
|
||||
size={12}
|
||||
color={isDarkMode ? '#10B981' : '#059669'}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
onPress={cancelEditingLimit}
|
||||
className="ml-1 p-1"
|
||||
disabled={updatingLimit}
|
||||
>
|
||||
<FontAwesome5
|
||||
name="times"
|
||||
size={12}
|
||||
color={isDarkMode ? '#F87171' : '#EF4444'}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
) : (
|
||||
<TouchableOpacity
|
||||
onPress={() =>
|
||||
isAdmin && startEditingLimit(item.user_id, item.allocated_credits)
|
||||
}
|
||||
className={`mr-4 flex-row items-center ${isAdmin ? 'opacity-100' : 'opacity-80'}`}
|
||||
>
|
||||
<Text
|
||||
className={`text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`}
|
||||
>
|
||||
Zugewiesene Credits: {item.allocated_credits}
|
||||
</Text>
|
||||
{isAdmin && (
|
||||
<FontAwesome5
|
||||
name="edit"
|
||||
size={10}
|
||||
color={isDarkMode ? '#93C5FD' : '#0055FF'}
|
||||
style={{ marginLeft: 4 }}
|
||||
/>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<Text className={`text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`}>
|
||||
Genutzte Credits: {item.used_credits}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
{isAdmin && (
|
||||
<View className="flex-row">
|
||||
<TouchableOpacity
|
||||
className={`${isDarkMode ? 'bg-blue-900' : 'bg-blue-100'} mr-2 rounded-full p-2`}
|
||||
onPress={() => startEditingLimit(item.user_id, item.allocated_credits)}
|
||||
>
|
||||
<FontAwesome5
|
||||
name="sliders-h"
|
||||
size={16}
|
||||
color={isDarkMode ? '#93C5FD' : '#0055FF'}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
className={`${isDarkMode ? 'bg-red-900' : 'bg-red-100'} rounded-full p-2`}
|
||||
onPress={() =>
|
||||
removeMember(
|
||||
item.user_id,
|
||||
item.first_name && item.last_name
|
||||
? `${item.first_name} ${item.last_name}`
|
||||
: `Benutzer ${item.user_id.substring(0, 8)}...`
|
||||
)
|
||||
}
|
||||
>
|
||||
<FontAwesome5
|
||||
name="user-minus"
|
||||
size={16}
|
||||
color={isDarkMode ? '#F87171' : '#EF4444'}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<View className="items-center justify-center py-8">
|
||||
<FontAwesome5 name="users" size={50} color={isDarkMode ? '#4B5563' : '#ccc'} />
|
||||
<Text
|
||||
className={`text-base ${isDarkMode ? 'text-gray-300' : 'text-gray-600'} mt-4 text-center`}
|
||||
>
|
||||
Die Team-Mitgliederverwaltung wird derzeit auf das neue Auth-System migriert und ist
|
||||
bald wieder verfügbar.
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
|
|
|||
228
apps/manacore/apps/mobile/context/AuthProvider.tsx
Normal file
228
apps/manacore/apps/mobile/context/AuthProvider.tsx
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||
import { ActivityIndicator, View, Text } from 'react-native';
|
||||
import * as SecureStore from 'expo-secure-store';
|
||||
import {
|
||||
createAuthService,
|
||||
createTokenManager,
|
||||
setStorageAdapter,
|
||||
setDeviceAdapter,
|
||||
setNetworkAdapter,
|
||||
createMemoryStorageAdapter,
|
||||
type UserData,
|
||||
} from '@manacore/shared-auth';
|
||||
|
||||
// Mana Core Auth URL from environment
|
||||
const MANA_AUTH_URL = process.env.EXPO_PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001';
|
||||
|
||||
// Create SecureStore adapter for React Native
|
||||
const createSecureStoreAdapter = () => ({
|
||||
async getItem<T>(key: string): Promise<T | null> {
|
||||
try {
|
||||
const value = await SecureStore.getItemAsync(key);
|
||||
return value ? JSON.parse(value) : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
async setItem(key: string, value: unknown): Promise<void> {
|
||||
await SecureStore.setItemAsync(key, JSON.stringify(value));
|
||||
},
|
||||
async removeItem(key: string): Promise<void> {
|
||||
await SecureStore.deleteItemAsync(key);
|
||||
},
|
||||
});
|
||||
|
||||
// Create device adapter for React Native
|
||||
const createReactNativeDeviceAdapter = () => {
|
||||
let deviceId: string | null = null;
|
||||
|
||||
return {
|
||||
async getDeviceInfo() {
|
||||
if (!deviceId) {
|
||||
// Try to get stored device ID
|
||||
deviceId = await SecureStore.getItemAsync('@device/id');
|
||||
|
||||
if (!deviceId) {
|
||||
// Generate new device ID
|
||||
deviceId = `rn-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
await SecureStore.setItemAsync('@device/id', deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
deviceId,
|
||||
deviceName: 'React Native Device',
|
||||
platform: 'react-native',
|
||||
};
|
||||
},
|
||||
async getStoredDeviceId() {
|
||||
return deviceId || (await SecureStore.getItemAsync('@device/id'));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
// Create network adapter (basic implementation)
|
||||
const createReactNativeNetworkAdapter = () => ({
|
||||
async isConnected() {
|
||||
return true; // Always assume connected for now
|
||||
},
|
||||
async hasStableConnection() {
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
// Initialize adapters
|
||||
setStorageAdapter(createSecureStoreAdapter());
|
||||
setDeviceAdapter(createReactNativeDeviceAdapter());
|
||||
setNetworkAdapter(createReactNativeNetworkAdapter());
|
||||
|
||||
// Create auth service
|
||||
const authService = createAuthService({ baseUrl: MANA_AUTH_URL });
|
||||
const tokenManager = createTokenManager(authService);
|
||||
|
||||
// Auth context type
|
||||
type AuthContextType = {
|
||||
user: UserData | null;
|
||||
loading: boolean;
|
||||
signIn: (email: string, password: string) => Promise<{ error: any | null }>;
|
||||
signUp: (email: string, password: string) => Promise<{ error: any | null; data: any | null }>;
|
||||
signOut: () => Promise<void>;
|
||||
resetPassword: (email: string) => Promise<{ error: any | null }>;
|
||||
};
|
||||
|
||||
// Create auth context
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
|
||||
// Hook to access auth context
|
||||
export const useAuth = () => {
|
||||
const context = useContext(AuthContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useAuth must be used within an AuthProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
// AuthProvider component
|
||||
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
const [user, setUser] = useState<UserData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// Initialize auth state
|
||||
useEffect(() => {
|
||||
const initialize = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// Check if user is authenticated
|
||||
const authenticated = await authService.isAuthenticated();
|
||||
|
||||
if (authenticated) {
|
||||
const userData = await authService.getUserFromToken();
|
||||
setUser(userData);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Initialisieren der Auth-Session:', error);
|
||||
setUser(null);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
initialize();
|
||||
}, []);
|
||||
|
||||
// Sign in with email and password
|
||||
const signIn = async (email: string, password: string) => {
|
||||
try {
|
||||
const result = await authService.signIn(email, password);
|
||||
|
||||
if (!result.success) {
|
||||
return { error: { message: result.error } };
|
||||
}
|
||||
|
||||
// Get user data from token
|
||||
const userData = await authService.getUserFromToken();
|
||||
setUser(userData);
|
||||
|
||||
return { error: null };
|
||||
} catch (error: any) {
|
||||
console.error('Fehler beim Anmelden:', error.message || error);
|
||||
return { error };
|
||||
}
|
||||
};
|
||||
|
||||
// Sign up with email and password
|
||||
const signUp = async (email: string, password: string) => {
|
||||
try {
|
||||
const result = await authService.signUp(email, password);
|
||||
|
||||
if (!result.success) {
|
||||
return { data: null, error: { message: result.error } };
|
||||
}
|
||||
|
||||
// Auto sign in after successful signup
|
||||
const signInResult = await signIn(email, password);
|
||||
|
||||
if (signInResult.error) {
|
||||
return { data: null, error: signInResult.error };
|
||||
}
|
||||
|
||||
return { data: user, error: null };
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Registrieren:', error);
|
||||
return { data: null, error };
|
||||
}
|
||||
};
|
||||
|
||||
// Sign out
|
||||
const signOut = async () => {
|
||||
try {
|
||||
await authService.signOut();
|
||||
setUser(null);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Abmelden:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Reset password
|
||||
const resetPassword = async (email: string) => {
|
||||
try {
|
||||
const result = await authService.forgotPassword(email);
|
||||
|
||||
if (!result.success) {
|
||||
return { error: { message: result.error } };
|
||||
}
|
||||
|
||||
return { error: null };
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Zurücksetzen des Passworts:', error);
|
||||
return { error };
|
||||
}
|
||||
};
|
||||
|
||||
// Show loading indicator during initialization
|
||||
if (loading) {
|
||||
return (
|
||||
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
|
||||
<ActivityIndicator size="large" color="#F59E0B" />
|
||||
<Text style={{ marginTop: 16 }}>Authentifizierung wird initialisiert...</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// Provide auth context
|
||||
return (
|
||||
<AuthContext.Provider
|
||||
value={{
|
||||
user,
|
||||
loading,
|
||||
signIn,
|
||||
signUp,
|
||||
signOut,
|
||||
resetPassword,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
209
apps/manacore/apps/mobile/services/api.ts
Normal file
209
apps/manacore/apps/mobile/services/api.ts
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
import * as SecureStore from 'expo-secure-store';
|
||||
|
||||
const BASE_URL = process.env.EXPO_PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001';
|
||||
|
||||
/**
|
||||
* API client for mana-core-auth service.
|
||||
* Wraps fetch calls and automatically attaches JWT from SecureStore.
|
||||
*/
|
||||
class ManaCoreApi {
|
||||
private async getToken(): Promise<string | null> {
|
||||
try {
|
||||
const raw = await SecureStore.getItemAsync('@manacore/app_token');
|
||||
if (!raw) return null;
|
||||
// The token might be stored as a JSON string
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
return typeof parsed === 'string' ? parsed : (parsed?.accessToken ?? raw);
|
||||
} catch {
|
||||
return raw;
|
||||
}
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async request<T>(
|
||||
path: string,
|
||||
options: RequestInit = {}
|
||||
): Promise<{ data: T | null; error: string | null }> {
|
||||
try {
|
||||
const token = await this.getToken();
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
...(options.headers as Record<string, string>),
|
||||
};
|
||||
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
const response = await fetch(`${BASE_URL}${path}`, {
|
||||
...options,
|
||||
headers,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
return {
|
||||
data: null,
|
||||
error: errorData.message || errorData.error || `HTTP ${response.status}`,
|
||||
};
|
||||
}
|
||||
|
||||
// Handle 204 No Content
|
||||
if (response.status === 204) {
|
||||
return { data: null, error: null };
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return { data, error: null };
|
||||
} catch (error: any) {
|
||||
return {
|
||||
data: null,
|
||||
error: error.message || 'Netzwerkfehler. Bitte versuchen Sie es später erneut.',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Profile ----
|
||||
|
||||
async getProfile() {
|
||||
return this.request<{
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
image?: string;
|
||||
createdAt: string;
|
||||
}>('/api/v1/auth/profile');
|
||||
}
|
||||
|
||||
async updateProfile(data: { name?: string; image?: string }) {
|
||||
return this.request('/api/v1/auth/profile', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
// ---- Organizations ----
|
||||
|
||||
async getOrganizations() {
|
||||
return this.request<
|
||||
Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
logo?: string;
|
||||
createdAt: string;
|
||||
metadata?: Record<string, any>;
|
||||
}>
|
||||
>('/api/v1/auth/organizations');
|
||||
}
|
||||
|
||||
async getOrganization(id: string) {
|
||||
return this.request<{
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
logo?: string;
|
||||
createdAt: string;
|
||||
metadata?: Record<string, any>;
|
||||
}>(`/api/v1/auth/organizations/${id}`);
|
||||
}
|
||||
|
||||
async createOrganization(data: { name: string; slug?: string }) {
|
||||
return this.request('/api/v1/auth/register/b2b', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
async deleteOrganization(id: string) {
|
||||
return this.request(`/api/v1/auth/organizations/${id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
// ---- Organization Members ----
|
||||
|
||||
async getOrgMembers(orgId: string) {
|
||||
return this.request<
|
||||
Array<{
|
||||
id: string;
|
||||
userId: string;
|
||||
email?: string;
|
||||
name?: string;
|
||||
role: string;
|
||||
createdAt: string;
|
||||
}>
|
||||
>(`/api/v1/auth/organizations/${orgId}/members`);
|
||||
}
|
||||
|
||||
async inviteMember(orgId: string, email: string, role: string = 'member') {
|
||||
return this.request(`/api/v1/auth/organizations/${orgId}/invite`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ email, role }),
|
||||
});
|
||||
}
|
||||
|
||||
async removeMember(orgId: string, memberId: string) {
|
||||
return this.request(`/api/v1/auth/organizations/${orgId}/members/${memberId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
async updateMemberRole(orgId: string, memberId: string, role: string) {
|
||||
return this.request(`/api/v1/auth/organizations/${orgId}/members/${memberId}/role`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ role }),
|
||||
});
|
||||
}
|
||||
|
||||
// ---- Credits ----
|
||||
|
||||
async getCreditBalance() {
|
||||
return this.request<{
|
||||
balance: number;
|
||||
totalCredits: number;
|
||||
usedCredits: number;
|
||||
}>('/api/v1/credits/balance');
|
||||
}
|
||||
|
||||
async getCreditTransactions() {
|
||||
return this.request<
|
||||
Array<{
|
||||
id: string;
|
||||
amount: number;
|
||||
description: string;
|
||||
createdAt: string;
|
||||
type: string;
|
||||
}>
|
||||
>('/api/v1/credits/transactions');
|
||||
}
|
||||
|
||||
// ---- Invitations ----
|
||||
|
||||
async getInvitations() {
|
||||
return this.request<
|
||||
Array<{
|
||||
id: string;
|
||||
organizationId: string;
|
||||
organizationName: string;
|
||||
email: string;
|
||||
role: string;
|
||||
status: string;
|
||||
expiresAt: string;
|
||||
}>
|
||||
>('/api/v1/auth/invitations');
|
||||
}
|
||||
|
||||
async acceptInvitation(invitationId: string) {
|
||||
return this.request('/api/v1/auth/organizations/accept-invitation', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ invitationId }),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Export a singleton instance
|
||||
export const api = new ManaCoreApi();
|
||||
|
|
@ -1,121 +0,0 @@
|
|||
/**
|
||||
* Hybrid storage solution for Supabase authentication
|
||||
* Uses localStorage in browser environments and memory storage in SSR
|
||||
* This avoids issues with AsyncStorage during server-side rendering
|
||||
*/
|
||||
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
|
||||
interface StorageData {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
class HybridStorageService {
|
||||
private memoryStorage: StorageData = {};
|
||||
private isAsyncStorageAvailable = false;
|
||||
|
||||
constructor() {
|
||||
// Check if we're in an environment where AsyncStorage is available
|
||||
this.checkAsyncStorageAvailability();
|
||||
}
|
||||
|
||||
private async checkAsyncStorageAvailability(): Promise<void> {
|
||||
try {
|
||||
// Check if we're in a browser environment
|
||||
if (typeof window === 'undefined') {
|
||||
this.isAsyncStorageAvailable = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to access AsyncStorage
|
||||
this.isAsyncStorageAvailable = typeof AsyncStorage !== 'undefined';
|
||||
|
||||
// If available, try to load any existing auth data into memory
|
||||
if (this.isAsyncStorageAvailable) {
|
||||
await this.syncFromAsyncStorage();
|
||||
}
|
||||
} catch (_error) {
|
||||
console.warn('AsyncStorage not available, falling back to memory storage');
|
||||
this.isAsyncStorageAvailable = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async syncFromAsyncStorage(): Promise<void> {
|
||||
if (typeof window === 'undefined') {
|
||||
// Skip AsyncStorage sync in SSR environment
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Get all keys that start with 'supabase.auth'
|
||||
const allKeys = await AsyncStorage.getAllKeys();
|
||||
const authKeys = allKeys.filter((key) => key.startsWith('supabase.auth'));
|
||||
|
||||
if (authKeys.length > 0) {
|
||||
const keyValuePairs = await AsyncStorage.multiGet(authKeys);
|
||||
keyValuePairs.forEach(([key, value]) => {
|
||||
if (value) {
|
||||
this.memoryStorage[key] = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error syncing from AsyncStorage:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async getItem(key: string): Promise<string | null> {
|
||||
// First check memory storage
|
||||
const memoryValue = this.memoryStorage[key];
|
||||
if (memoryValue) return memoryValue;
|
||||
|
||||
// If not in memory and AsyncStorage is available, try to get from there
|
||||
if (this.isAsyncStorageAvailable && typeof window !== 'undefined') {
|
||||
try {
|
||||
const value = await AsyncStorage.getItem(key);
|
||||
if (value) {
|
||||
// Update memory cache
|
||||
this.memoryStorage[key] = value;
|
||||
return value;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error reading from AsyncStorage:', error);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async setItem(key: string, value: string): Promise<void> {
|
||||
// Always update memory storage
|
||||
this.memoryStorage[key] = value;
|
||||
|
||||
// If AsyncStorage is available, also persist there
|
||||
if (this.isAsyncStorageAvailable && typeof window !== 'undefined') {
|
||||
try {
|
||||
await AsyncStorage.setItem(key, value);
|
||||
} catch (error) {
|
||||
console.error('Error writing to AsyncStorage:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async removeItem(key: string): Promise<void> {
|
||||
// Remove from memory storage
|
||||
delete this.memoryStorage[key];
|
||||
|
||||
// If AsyncStorage is available, also remove from there
|
||||
if (this.isAsyncStorageAvailable && typeof window !== 'undefined') {
|
||||
try {
|
||||
await AsyncStorage.removeItem(key);
|
||||
} catch (error) {
|
||||
console.error('Error removing from AsyncStorage:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a singleton instance
|
||||
const hybridStorage = new HybridStorageService();
|
||||
|
||||
export default hybridStorage;
|
||||
|
|
@ -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,
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue