mirror of
https://github.com/mudabbir-ahmad/MAD-Treasure-Hunt.git
synced 2026-10-07 19:00:20 +00:00
Merge pull request #6 from mudabbir-ahmad/Business-stuff
Business-stuff
This commit is contained in:
20 files changed
+1437
-660
No files matched your search
@@ -9,6 +9,7 @@ import TeamScreen from "./src/components/screens/TeamScreen";
|
||||
import MapScreen from "./src/components/screens/MapScreen";
|
||||
import LeaderboardScreen from "./src/components/screens/LeaderboardScreen";
|
||||
import GameSettingsScreen from "./src/components/screens/GameSettingsScreen";
|
||||
import DepartmentSettingsScreen from "./src/components/screens/DepartmentSettingsScreen";
|
||||
import PlayersScreen from "./src/components/screens/PlayersScreen";
|
||||
import ExpandedMapScreen from "./src/components/screens/ExpandedMapScreen";
|
||||
import {loadSession} from "./src/hooks/SessionStore";
|
||||
@@ -43,6 +44,7 @@ const GameStack = () => (
|
||||
const AdminStack = () => (
|
||||
<AdminStackNav.Navigator screenOptions={screenOptions}>
|
||||
<AdminStackNav.Screen name="GameSettingsScreen" component={GameSettingsScreen}/>
|
||||
<AdminStackNav.Screen name="DepartmentSettingsScreen" component={DepartmentSettingsScreen}/>
|
||||
<AdminStackNav.Screen name="MapScreen" component={MapScreen}/>
|
||||
<AdminStackNav.Screen name="LeaderboardScreen" component={LeaderboardScreen}/>
|
||||
<AdminStackNav.Screen name="PlayersScreen" component={PlayersScreen}/>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 8.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
@@ -13,12 +13,12 @@ const Card = ({children, style}) => {
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
backgroundColor: '#ffffff',
|
||||
backgroundColor: '#313244',
|
||||
borderRadius: 8,
|
||||
padding: 12,
|
||||
marginBottom: 8,
|
||||
borderWidth: 1,
|
||||
borderColor: '#e5e7eb',
|
||||
borderColor: '#45475a',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -2,31 +2,28 @@ import {StyleSheet, Text, View} from 'react-native';
|
||||
import Card from '../UI/Card';
|
||||
import {Button} from '../UI/Button';
|
||||
|
||||
const CacheCardItem = ({cache, isAdmin, isClaimed, isSelected, onEdit, onDelete, onSelect, disabled}) => {
|
||||
// Initialisation -------------
|
||||
const CacheCardItem = ({cache, isAdmin, isClaimed, isSelected, onEdit, onDelete, onSelect, disabled, departmentName}) => {
|
||||
// Display name fallback for cache without a name
|
||||
const displayName = cache.name || cache.clue || 'Unnamed Cache';
|
||||
|
||||
const displayName = cache.name || cache.clue || 'Unnamed Cache';
|
||||
|
||||
// State ----------------------
|
||||
// Handlers -------------------
|
||||
// View -----------------------
|
||||
|
||||
return (
|
||||
<Card style={[isSelected && styles.selectedCard, isClaimed && styles.claimedCard, disabled && styles.disabledCard]}>
|
||||
<View style={styles.topRow}>
|
||||
<View style={styles.nameWrap}>
|
||||
<Text style={[styles.name, isClaimed && styles.claimedText, disabled && styles.disabledText]} numberOfLines={1}>{displayName}</Text>
|
||||
{cache.name ? (
|
||||
<Text style={styles.clue} numberOfLines={1}>{cache.clue}</Text>
|
||||
) : null}
|
||||
</View>
|
||||
{/* Players see team-specific claim status; admins see no status */}
|
||||
{!isAdmin && !disabled && (
|
||||
<Text style={[styles.status, {color: isClaimed ? '#9ca3af' : '#16a34a'}]}>
|
||||
{isClaimed ? 'Claimed' : 'Available'}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
return (
|
||||
<Card style={[isSelected && styles.selectedCard, isClaimed && styles.claimedCard, disabled && styles.disabledCard]}>
|
||||
<View style={styles.topRow}>
|
||||
<View style={styles.nameWrap}>
|
||||
<Text style={[styles.name, isClaimed && styles.claimedText, disabled && styles.disabledText]} numberOfLines={1}>{displayName}</Text>
|
||||
{isAdmin && departmentName ? (
|
||||
<Text style={styles.departmentText} numberOfLines={1}>Department: {departmentName}</Text>
|
||||
) : null}
|
||||
{cache.name ? (
|
||||
<Text style={styles.clue} numberOfLines={1}>{cache.clue}</Text>
|
||||
) : null}
|
||||
</View>
|
||||
{!isAdmin && !disabled && (
|
||||
<Text style={[styles.status, {color: isClaimed ? '#585b70' : '#a6e3a1'}]}>
|
||||
{isClaimed ? 'Claimed' : 'Available'}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
<View style={styles.actions}>
|
||||
{isAdmin ? (
|
||||
<>
|
||||
@@ -86,54 +83,55 @@ const styles = StyleSheet.create({
|
||||
alignItems: 'center',
|
||||
marginBottom: 8,
|
||||
},
|
||||
clue: {fontSize: 13, color: '#6b7280', marginTop: 2},
|
||||
clue: {fontSize: 13, color: '#9ca3af', marginTop: 2},
|
||||
departmentText: {fontSize: 13, color: '#bd93f9', marginTop: 2, fontWeight: '600'},
|
||||
nameWrap: {flex: 1, marginRight: 8},
|
||||
name: {fontSize: 15, fontWeight: '600', color: '#1f2937'},
|
||||
claimedText: {color: '#9ca3af'},
|
||||
name: {fontSize: 15, fontWeight: '600', color: '#cdd6f4'},
|
||||
claimedText: {color: '#6c7086'},
|
||||
status: {fontSize: 13, fontWeight: '600'},
|
||||
actions: {flexDirection: 'row', gap: 8},
|
||||
editBtn: {
|
||||
backgroundColor: '#2563eb',
|
||||
borderColor: '#2563eb',
|
||||
backgroundColor: '#bd93f9',
|
||||
borderColor: '#bd93f9',
|
||||
minHeight: 36,
|
||||
paddingHorizontal: 14,
|
||||
flex: 1,
|
||||
},
|
||||
deleteBtn: {
|
||||
backgroundColor: '#dc2626',
|
||||
borderColor: '#dc2626',
|
||||
backgroundColor: '#D92800',
|
||||
borderColor: '#D92800',
|
||||
minHeight: 36,
|
||||
paddingHorizontal: 14,
|
||||
flex: 1,
|
||||
},
|
||||
selectBtn: {
|
||||
backgroundColor: '#16a34a',
|
||||
borderColor: '#16a34a',
|
||||
backgroundColor: '#a6e3a1',
|
||||
borderColor: '#a6e3a1',
|
||||
minHeight: 36,
|
||||
paddingHorizontal: 14,
|
||||
flex: 1,
|
||||
},
|
||||
selectedBtn: {
|
||||
backgroundColor: '#6b7280',
|
||||
borderColor: '#6b7280',
|
||||
backgroundColor: '#6c7086',
|
||||
borderColor: '#6c7086',
|
||||
minHeight: 36,
|
||||
paddingHorizontal: 14,
|
||||
flex: 1,
|
||||
},
|
||||
claimedBtn: {
|
||||
backgroundColor: '#d1d5db',
|
||||
borderColor: '#d1d5db',
|
||||
backgroundColor: '#45475a',
|
||||
borderColor: '#45475a',
|
||||
minHeight: 36,
|
||||
paddingHorizontal: 14,
|
||||
flex: 1,
|
||||
},
|
||||
claimedBtnLabel: {color: '#6b7280', fontWeight: '600', fontSize: 13},
|
||||
selectedBtnLabel: {color: '#ffffff', fontWeight: '600', fontSize: 13},
|
||||
selectedCard: {borderColor: '#2563eb', borderWidth: 2},
|
||||
claimedBtnLabel: {color: '#6c7086', fontWeight: '600', fontSize: 13},
|
||||
selectedBtnLabel: {color: '#cdd6f4', fontWeight: '600', fontSize: 13},
|
||||
selectedCard: {borderColor: '#bd93f9', borderWidth: 2},
|
||||
claimedCard: {opacity: 0.5},
|
||||
disabledCard: {opacity: 0.45},
|
||||
disabledText: {color: '#9ca3af'},
|
||||
btnLabel: {color: '#ffffff', fontWeight: '600', fontSize: 13},
|
||||
disabledText: {color: '#6c7086'},
|
||||
btnLabel: {color: '#cdd6f4', fontWeight: '600', fontSize: 13},
|
||||
});
|
||||
|
||||
export default CacheCardItem;
|
||||
|
||||
@@ -3,15 +3,11 @@ import {StyleSheet, Text, View} from 'react-native';
|
||||
import {CACHE_CLAIM_TIMER} from '../../utils/geoMath';
|
||||
|
||||
const ClaimTimerView = ({ cache, onClaimSuccess, isClaiming, showClaimedPopup }) => {
|
||||
// Initialisation -------------
|
||||
// State ----------------------
|
||||
|
||||
const [timeLeft, setTimeLeft] = useState(CACHE_CLAIM_TIMER);
|
||||
// Use a ref for the callback so it does not appear in the dependency array
|
||||
const onClaimRef = useRef(onClaimSuccess);
|
||||
onClaimRef.current = onClaimSuccess;
|
||||
|
||||
// Handlers -------------------
|
||||
|
||||
useEffect(() => {
|
||||
let timer;
|
||||
@@ -25,7 +21,6 @@ const ClaimTimerView = ({ cache, onClaimSuccess, isClaiming, showClaimedPopup })
|
||||
return () => clearTimeout(timer);
|
||||
}, [isClaiming, timeLeft, cache]);
|
||||
|
||||
// View -----------------------
|
||||
|
||||
// Show "Cache Claimed!" popup after a successful claim
|
||||
if (showClaimedPopup) {
|
||||
@@ -50,27 +45,27 @@ const ClaimTimerView = ({ cache, onClaimSuccess, isClaiming, showClaimedPopup })
|
||||
const styles = StyleSheet.create({
|
||||
overlay: {
|
||||
alignSelf: 'center',
|
||||
backgroundColor: 'rgba(0,0,0,0.75)',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.75)',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
borderRadius: 10,
|
||||
marginTop: 8,
|
||||
},
|
||||
text: {
|
||||
color: '#ffffff',
|
||||
color: '#cdd6f4',
|
||||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
},
|
||||
claimedOverlay: {
|
||||
alignSelf: 'center',
|
||||
backgroundColor: '#16a34a',
|
||||
backgroundColor: '#a6e3a1',
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 14,
|
||||
borderRadius: 10,
|
||||
marginTop: 8,
|
||||
},
|
||||
claimedText: {
|
||||
color: '#ffffff',
|
||||
color: '#1e1e2e',
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
},
|
||||
|
||||
@@ -3,17 +3,29 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
import {clearGameSession, getSession} from '../../hooks/SessionStore';
|
||||
import API, {API_BASE_URL} from '../API/API';
|
||||
|
||||
const BottomNavbar = ({navigation, routeName}) => {
|
||||
const BottomNavbar = ({navigation, routeName, routeParams}) => {
|
||||
// Initialisation ------------
|
||||
|
||||
const insets = useSafeAreaInsets();
|
||||
const session = getSession();
|
||||
|
||||
const selectedDepartment = routeName === 'DepartmentSettingsScreen' && routeParams?.SGid
|
||||
? {SGid: routeParams.SGid, SubGroupName: routeParams.SubGroupName || routeParams.selectedDepartmentName || 'Department'}
|
||||
: null;
|
||||
|
||||
const adminTabs = [
|
||||
{label: 'Settings', route: 'GameSettingsScreen', onPress: () => navigation.navigate('GameSettingsScreen')},
|
||||
{label: 'Map', route: 'MapScreen', onPress: () => navigation.navigate('MapScreen')},
|
||||
{label: 'Players', route: 'PlayersScreen', onPress: () => navigation.navigate('PlayersScreen')},
|
||||
{label: 'Leaderboard', route: 'LeaderboardScreen', onPress: () => navigation.navigate('LeaderboardScreen')},
|
||||
{
|
||||
label: 'Players',
|
||||
route: 'PlayersScreen',
|
||||
onPress: () => navigation.navigate('PlayersScreen', selectedDepartment ? {department: selectedDepartment} : undefined),
|
||||
},
|
||||
{
|
||||
label: 'Leaderboard',
|
||||
route: 'LeaderboardScreen',
|
||||
onPress: () => navigation.navigate('LeaderboardScreen', selectedDepartment ? {department: selectedDepartment} : undefined),
|
||||
},
|
||||
];
|
||||
|
||||
// Only show the Leave tab when the player is actually in a game
|
||||
@@ -78,8 +90,8 @@ const styles = StyleSheet.create({
|
||||
container: {
|
||||
minHeight: 64,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: '#555555',
|
||||
backgroundColor: '#4a4a4a',
|
||||
borderTopColor: '#45475a',
|
||||
backgroundColor: '#313244',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-around',
|
||||
@@ -91,26 +103,25 @@ const styles = StyleSheet.create({
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: 12,
|
||||
borderRadius: 8,
|
||||
marginHorizontal: 4,
|
||||
},
|
||||
tabActive: {
|
||||
backgroundColor: '#5c5c5c',
|
||||
backgroundColor: '#45475a',
|
||||
},
|
||||
tabLeave: {
|
||||
backgroundColor: 'rgba(220,38,38,0.15)',
|
||||
backgroundColor: 'rgba(243, 139, 168, 0.15)',
|
||||
},
|
||||
tabText: {
|
||||
color: '#d1d5db',
|
||||
color: '#bac2de',
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
textAlign: 'center',
|
||||
},
|
||||
tabTextActive: {
|
||||
color: '#ffffff',
|
||||
color: '#cdd6f4',
|
||||
},
|
||||
tabTextLeave: {
|
||||
color: '#fca5a5',
|
||||
color: '#D92800',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ const Screen = ({ children, style, showBack = false }) => {
|
||||
TeamScreen: "Current Team",
|
||||
MapScreen: "Map",
|
||||
GameSettingsScreen: "Game Settings",
|
||||
DepartmentSettingsScreen: "Department Settings",
|
||||
PlayersScreen: "Players",
|
||||
ExpandedMapScreen: "Map View",
|
||||
};
|
||||
@@ -34,7 +35,7 @@ const Screen = ({ children, style, showBack = false }) => {
|
||||
<View style={[styles.screen, isAuthScreen && {paddingTop: insets.top, paddingBottom: insets.bottom}]}>
|
||||
{!isAuthScreen && <TopNavbar title={pageTitle} showBack={showBack} navigation={navigation} />}
|
||||
<View style={[styles.content, style]}>{children}</View>
|
||||
{!isAuthScreen && <BottomNavbar navigation={navigation} routeName={routeName} />}
|
||||
{!isAuthScreen && <BottomNavbar navigation={navigation} routeName={routeName} routeParams={route.params} />}
|
||||
<StatusBar style="light" />
|
||||
</View>
|
||||
);
|
||||
@@ -43,13 +44,13 @@ const Screen = ({ children, style, showBack = false }) => {
|
||||
const styles = StyleSheet.create({
|
||||
screen: {
|
||||
flex: 1,
|
||||
backgroundColor: "#fff",
|
||||
backgroundColor: '#1e1e2e',
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
padding: 15,
|
||||
zIndex: 1,
|
||||
overflow: "hidden",
|
||||
overflow: 'hidden',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -47,12 +47,12 @@ const styles = StyleSheet.create({
|
||||
container: {
|
||||
minHeight: 56,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#333333',
|
||||
borderBottomColor: '#45475a',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: 12,
|
||||
backgroundColor: '#000000',
|
||||
backgroundColor: '#313244',
|
||||
zIndex: 10,
|
||||
},
|
||||
leftSlot: {
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {ActivityIndicator, Alert, ScrollView, StyleSheet, Switch, Text, TextInput, View} from 'react-native';
|
||||
import Screen from '../layout/Screen';
|
||||
import {Button, ButtonTray} from '../UI/Button';
|
||||
import useGameHook from '../../hooks/useGameHook';
|
||||
|
||||
const DepartmentSettingsScreen = ({route, navigation}) => {
|
||||
const {SGid, Gid} = route.params;
|
||||
const {getSubgroup, updateSubgroup, getCaches, resetGame} = useGameHook();
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [deptName, setDeptName] = useState('');
|
||||
const [joinCode, setJoinCode] = useState('');
|
||||
const [teamsEnabled, setTeamsEnabled] = useState(false);
|
||||
const [cacheTriggerMeters, setCacheTriggerMeters] = useState('20');
|
||||
const [cacheCount, setCacheCount] = useState(0);
|
||||
|
||||
// Handlers -------------------
|
||||
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
const sg = await getSubgroup(SGid);
|
||||
if (sg) {
|
||||
setDeptName(sg.SubGroupName || '');
|
||||
setJoinCode(sg.JoinCode || '');
|
||||
setTeamsEnabled(Boolean(sg.TeamsEnabled));
|
||||
setCacheTriggerMeters(String(sg.CacheTriggerMeters || 20));
|
||||
}
|
||||
// Load cache count for this department
|
||||
const caches = await getCaches(Gid, SGid);
|
||||
setCacheCount((caches || []).length);
|
||||
setLoading(false);
|
||||
};
|
||||
load();
|
||||
}, []);
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true);
|
||||
await updateSubgroup(SGid, {
|
||||
SubGroupName: deptName.trim(),
|
||||
TeamsEnabled: teamsEnabled,
|
||||
CacheTriggerMeters: parseInt(cacheTriggerMeters) || 20,
|
||||
});
|
||||
setSaving(false);
|
||||
Alert.alert('Saved', 'Department settings updated.');
|
||||
};
|
||||
|
||||
const handleResetDepartment = () => {
|
||||
Alert.alert(
|
||||
'Reset Department',
|
||||
'This will reset all cache claims for this department. Are you sure?',
|
||||
[
|
||||
{text: 'Cancel', style: 'cancel'},
|
||||
{
|
||||
text: 'Reset',
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
await resetGame(Gid, SGid);
|
||||
Alert.alert('Done', 'Department progress has been reset.');
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
};
|
||||
|
||||
const handleManageCaches = () => {
|
||||
navigation.navigate('MapScreen', {
|
||||
selectedDepartmentSGid: SGid,
|
||||
selectedDepartmentName: deptName || null,
|
||||
});
|
||||
};
|
||||
|
||||
// View -----------------------
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Screen showBack={true} style={styles.center}>
|
||||
<ActivityIndicator size="large"/>
|
||||
</Screen>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Screen showBack={true}>
|
||||
<ScrollView contentContainerStyle={styles.formContainer} showsVerticalScrollIndicator={false}>
|
||||
<Text style={styles.sectionTitle}>Department Settings</Text>
|
||||
|
||||
<Text style={styles.label}>Department Name</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={deptName}
|
||||
onChangeText={setDeptName}
|
||||
placeholder="Enter department name"
|
||||
placeholderTextColor="#9ca3af"
|
||||
/>
|
||||
|
||||
<View style={styles.codeSection}>
|
||||
<Text style={styles.codeLabel}>Department Join Code</Text>
|
||||
<Text style={styles.codeValue}>{joinCode || '—'}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.toggleRow}>
|
||||
<Text style={styles.toggleLabel}>Teams Enabled</Text>
|
||||
<Switch
|
||||
value={teamsEnabled}
|
||||
onValueChange={setTeamsEnabled}
|
||||
trackColor={{false: '#d1d5db', true: '#93c5fd'}}
|
||||
thumbColor={teamsEnabled ? '#2563eb' : '#9ca3af'}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Text style={styles.label}>Cache Claim Distance (metres)</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={cacheTriggerMeters}
|
||||
onChangeText={setCacheTriggerMeters}
|
||||
keyboardType="numeric"
|
||||
placeholder="20"
|
||||
placeholderTextColor="#9ca3af"
|
||||
/>
|
||||
|
||||
<View style={styles.infoRow}>
|
||||
<Text style={styles.infoLabel}>Total Caches</Text>
|
||||
<Text style={styles.infoValue}>{cacheCount}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.saveWrap}>
|
||||
<ButtonTray>
|
||||
<Button
|
||||
label={saving ? 'Saving...' : 'Save Settings'}
|
||||
onClick={handleSave}
|
||||
styleButton={styles.saveButton}
|
||||
styleLabel={styles.saveLabel}
|
||||
/>
|
||||
</ButtonTray>
|
||||
</View>
|
||||
|
||||
<View style={styles.mapWrap}>
|
||||
<ButtonTray>
|
||||
<Button
|
||||
label="Manage Caches"
|
||||
onClick={handleManageCaches}
|
||||
styleButton={styles.mapButton}
|
||||
styleLabel={styles.mapLabel}
|
||||
/>
|
||||
</ButtonTray>
|
||||
</View>
|
||||
|
||||
<View style={styles.resetWrap}>
|
||||
<Button
|
||||
label="Reset Department"
|
||||
onClick={handleResetDepartment}
|
||||
styleButton={styles.resetButton}
|
||||
styleLabel={styles.resetLabel}
|
||||
/>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</Screen>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
center: {justifyContent: 'center', alignItems: 'center'},
|
||||
formContainer: {paddingBottom: 20},
|
||||
sectionTitle: {fontSize: 20, fontWeight: '700', color: '#cdd6f4', marginBottom: 16},
|
||||
label: {fontSize: 14, fontWeight: '600', color: '#bac2de', marginBottom: 6, marginTop: 14},
|
||||
input: {
|
||||
borderWidth: 1,
|
||||
borderColor: '#45475a',
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 10,
|
||||
fontSize: 16,
|
||||
color: '#cdd6f4',
|
||||
backgroundColor: '#313244',
|
||||
},
|
||||
codeSection: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginTop: 16,
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 12,
|
||||
backgroundColor: '#313244',
|
||||
borderRadius: 8,
|
||||
},
|
||||
codeLabel: {fontSize: 14, fontWeight: '600', color: '#bac2de'},
|
||||
codeValue: {fontSize: 16, fontWeight: '700', color: '#bd93f9', letterSpacing: 2},
|
||||
toggleRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginTop: 16,
|
||||
paddingVertical: 10,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#45475a',
|
||||
},
|
||||
toggleLabel: {fontSize: 14, fontWeight: '600', color: '#bac2de'},
|
||||
infoRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginTop: 16,
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 12,
|
||||
backgroundColor: '#313244',
|
||||
borderRadius: 8,
|
||||
},
|
||||
infoLabel: {fontSize: 14, fontWeight: '600', color: '#bac2de'},
|
||||
infoValue: {fontSize: 16, fontWeight: '700', color: '#cdd6f4'},
|
||||
saveWrap: {marginTop: 24},
|
||||
saveButton: {backgroundColor: '#a6e3a1', borderColor: '#a6e3a1'},
|
||||
saveLabel: {color: '#1e1e2e', fontWeight: '600'},
|
||||
mapWrap: {marginTop: 16},
|
||||
mapButton: {backgroundColor: '#bd93f9', borderColor: '#bd93f9'},
|
||||
mapLabel: {color: '#1e1e2e', fontWeight: '600'},
|
||||
resetWrap: {marginTop: 30},
|
||||
resetButton: {backgroundColor: '#86efac', borderColor: '#86efac'},
|
||||
resetLabel: {color: '#1e1e2e', fontWeight: '600'},
|
||||
});
|
||||
|
||||
export default DepartmentSettingsScreen;
|
||||
|
||||
@@ -1,94 +1,163 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import React, {useCallback, useEffect, useRef, useState} from 'react';
|
||||
import {ActivityIndicator, Alert, ScrollView, StyleSheet, Switch, Text, TextInput, View} from 'react-native';
|
||||
import {useFocusEffect} from '@react-navigation/native';
|
||||
import Screen from '../layout/Screen';
|
||||
import Card from '../UI/Card';
|
||||
import {Button, ButtonTray} from '../UI/Button';
|
||||
import useGameHook from '../../hooks/useGameHook';
|
||||
import {getSession, setSessionTeamsEnabled} from '../../hooks/SessionStore';
|
||||
|
||||
const GameSettingsScreen = () => {
|
||||
// Initialisation ------------
|
||||
const GameSettingsScreen = ({navigation}) => {
|
||||
const session = getSession();
|
||||
const {getLobby, updateGroup, getSubgroups, createSubgroup, deleteSubgroup, resetGame, getAdminWaitlist, approveAdmin, rejectAdmin, getUser, disbandTeams} = useGameHook();
|
||||
const gameApiRef = useRef({
|
||||
getLobby,
|
||||
getSubgroups,
|
||||
getAdminWaitlist,
|
||||
getUser,
|
||||
});
|
||||
gameApiRef.current = {
|
||||
getLobby,
|
||||
getSubgroups,
|
||||
getAdminWaitlist,
|
||||
getUser,
|
||||
};
|
||||
const isMountedRef = useRef(true);
|
||||
const isRefreshingRef = useRef(false);
|
||||
const hasHydratedRef = useRef(false);
|
||||
|
||||
const session = getSession();
|
||||
const {getLobby, updateGroup, getSubgroups, createSubgroup, updateSubgroup, deleteSubgroup, resetGame, getAdminWaitlist, approveAdmin, rejectAdmin, getUser} = useGameHook();
|
||||
const isBusiness = Boolean(session.isBusiness);
|
||||
|
||||
// State ----------------------
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [groupName, setGroupName] = useState('');
|
||||
const [teamsEnabled, setTeamsEnabled] = useState(session.teamsEnabled);
|
||||
const [cacheTriggerMeters, setCacheTriggerMeters] = useState('20');
|
||||
const [adminJoinCode, setAdminJoinCode] = useState('');
|
||||
const [memberJoinCode, setMemberJoinCode] = useState('');
|
||||
const [waitlist, setWaitlist] = useState([]);
|
||||
const [waitlistNames, setWaitlistNames] = useState({});
|
||||
// Business subgroup management
|
||||
const [subgroupList, setSubgroupList] = useState([]);
|
||||
const [newDeptName, setNewDeptName] = useState('');
|
||||
const [editingSGid, setEditingSGid] = useState(null);
|
||||
const [editDeptTrigger, setEditDeptTrigger] = useState('20');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [groupName, setGroupName] = useState('');
|
||||
const [businessName, setBusinessName] = useState('');
|
||||
const [teamsEnabled, setTeamsEnabled] = useState(session.teamsEnabled);
|
||||
const [serverTeamsEnabled, setServerTeamsEnabled] = useState(session.teamsEnabled);
|
||||
const [cacheTriggerMeters, setCacheTriggerMeters] = useState('20');
|
||||
const [adminJoinCode, setAdminJoinCode] = useState('');
|
||||
const [orgJoinCode, setOrgJoinCode] = useState('');
|
||||
const [memberJoinCode, setMemberJoinCode] = useState('');
|
||||
const [waitlist, setWaitlist] = useState([]);
|
||||
const [waitlistNames, setWaitlistNames] = useState({});
|
||||
const [isBusinessGroup, setIsBusinessGroup] = useState(Boolean(session.isBusiness));
|
||||
const [subgroupList, setSubgroupList] = useState([]);
|
||||
const [newDeptName, setNewDeptName] = useState('');
|
||||
|
||||
// Handlers -------------------
|
||||
|
||||
const loadWaitlist = async () => {
|
||||
if (!session.currentGid) return;
|
||||
const rows = await getAdminWaitlist(session.currentGid);
|
||||
setWaitlist(rows || []);
|
||||
const names = {};
|
||||
for (const entry of (rows || [])) {
|
||||
const u = await getUser(entry.Uid);
|
||||
if (u) names[entry.Uid] = u.username;
|
||||
}
|
||||
setWaitlistNames(names);
|
||||
};
|
||||
useEffect(() => () => {
|
||||
isMountedRef.current = false;
|
||||
}, []);
|
||||
|
||||
const loadSubgroups = async () => {
|
||||
if (!session.currentGid) return;
|
||||
const sgs = await getSubgroups(session.currentGid);
|
||||
setSubgroupList(sgs || []);
|
||||
// Set the default member join code from first non-admin subgroup
|
||||
const memberSg = (sgs || []).find((sg) => !sg.IsAdminGroup);
|
||||
if (memberSg) setMemberJoinCode(memberSg.JoinCode || '');
|
||||
};
|
||||
const refreshSettings = useCallback(async ({showLoader = false} = {}) => {
|
||||
const activeSession = getSession();
|
||||
if (!activeSession.currentGid) {
|
||||
if (isMountedRef.current) setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Guard against re-entrant focus refresh calls.
|
||||
if (isRefreshingRef.current) return;
|
||||
isRefreshingRef.current = true;
|
||||
|
||||
if (showLoader && isMountedRef.current) setLoading(true);
|
||||
|
||||
try {
|
||||
const [group, sgs, waitlistRows] = await Promise.all([
|
||||
gameApiRef.current.getLobby(activeSession.currentGid),
|
||||
gameApiRef.current.getSubgroups(activeSession.currentGid),
|
||||
gameApiRef.current.getAdminWaitlist(activeSession.currentGid),
|
||||
]);
|
||||
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
if (!session.currentGid) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
const group = await getLobby(session.currentGid);
|
||||
if (group) {
|
||||
setGroupName(group.GroupName || '');
|
||||
setBusinessName(group.BusinessOrSchoolName || '');
|
||||
// Organisation mode
|
||||
setIsBusinessGroup(Boolean(group.BusinessOrSchoolName));
|
||||
|
||||
const serverTeams = Boolean(group.TeamsEnabled);
|
||||
setTeamsEnabled(serverTeams);
|
||||
// Keep session in sync so the toggle survives app reloads
|
||||
setServerTeamsEnabled(serverTeams);
|
||||
setSessionTeamsEnabled(serverTeams);
|
||||
|
||||
setCacheTriggerMeters(String(group.CacheTriggerMeters || 20));
|
||||
setAdminJoinCode(group.AdminJoinCode || '');
|
||||
setOrgJoinCode(group.OrgJoinCode || '');
|
||||
}
|
||||
await loadSubgroups();
|
||||
await loadWaitlist();
|
||||
setLoading(false);
|
||||
};
|
||||
load();
|
||||
|
||||
setSubgroupList(sgs || []);
|
||||
const memberSg = (sgs || []).find((sg) => !sg.IsAdminGroup);
|
||||
setMemberJoinCode(memberSg?.JoinCode || '');
|
||||
|
||||
setWaitlist(waitlistRows || []);
|
||||
const names = {};
|
||||
for (const entry of (waitlistRows || [])) {
|
||||
const user = await gameApiRef.current.getUser(entry.Uid);
|
||||
if (user) names[entry.Uid] = user.username;
|
||||
}
|
||||
if (!isMountedRef.current) return;
|
||||
setWaitlistNames(names);
|
||||
hasHydratedRef.current = true;
|
||||
} finally {
|
||||
if (isMountedRef.current && (showLoader || !hasHydratedRef.current)) {
|
||||
setLoading(false);
|
||||
}
|
||||
isRefreshingRef.current = false;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!session.currentGid) return;
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
refreshSettings({showLoader: !hasHydratedRef.current});
|
||||
}, [refreshSettings]),
|
||||
);
|
||||
|
||||
// --- Individual save ---
|
||||
const saveSettings = async () => {
|
||||
setSaving(true);
|
||||
const result = await updateGroup(session.currentGid, {
|
||||
GroupName: groupName.trim(),
|
||||
TeamsEnabled: teamsEnabled,
|
||||
CacheTriggerMeters: parseInt(cacheTriggerMeters) || 20,
|
||||
});
|
||||
// Persist the new toggle value so the session reflects it after a reload
|
||||
if (result) setSessionTeamsEnabled(teamsEnabled);
|
||||
if (result) {
|
||||
if (serverTeamsEnabled && !teamsEnabled) {
|
||||
await disbandTeams(session.currentGid);
|
||||
}
|
||||
setSessionTeamsEnabled(teamsEnabled);
|
||||
setServerTeamsEnabled(teamsEnabled);
|
||||
}
|
||||
setSaving(false);
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!session.currentGid) return;
|
||||
// Business save — only save business name
|
||||
if (isBusinessGroup) {
|
||||
setSaving(true);
|
||||
await updateGroup(session.currentGid, {
|
||||
BusinessOrSchoolName: businessName.trim(),
|
||||
GroupName: businessName.trim(),
|
||||
});
|
||||
setSaving(false);
|
||||
return;
|
||||
}
|
||||
// Individual save — check teams toggle
|
||||
if (serverTeamsEnabled && !teamsEnabled) {
|
||||
Alert.alert(
|
||||
'Disable Teams',
|
||||
'This will disband all existing teams and remove all players from their teams. Continue?',
|
||||
[
|
||||
{text: 'Cancel', style: 'cancel'},
|
||||
{text: 'Disable', style: 'destructive', onPress: saveSettings},
|
||||
],
|
||||
);
|
||||
return;
|
||||
}
|
||||
await saveSettings();
|
||||
};
|
||||
|
||||
const handleResetGame = () => {
|
||||
Alert.alert('Reset Entire Game', 'This will remove ALL saved caches and reset progress for every player. Are you sure?', [
|
||||
{text: 'Cancel', style: 'cancel'},
|
||||
@@ -105,32 +174,27 @@ const GameSettingsScreen = () => {
|
||||
|
||||
const handleApproveAdmin = async (entry) => {
|
||||
await approveAdmin(entry.id);
|
||||
await loadWaitlist();
|
||||
await refreshSettings();
|
||||
};
|
||||
|
||||
const handleRejectAdmin = async (entry) => {
|
||||
await rejectAdmin(entry.id);
|
||||
await loadWaitlist();
|
||||
await refreshSettings();
|
||||
};
|
||||
|
||||
// Business — create a new department (subgroup)
|
||||
const handleCreateDepartment = async () => {
|
||||
if (!newDeptName.trim() || !session.currentGid) return;
|
||||
const deptCount = subgroupList.filter((sg) => !sg.IsAdminGroup).length;
|
||||
const name = newDeptName.trim() || `Department ${deptCount + 1}`;
|
||||
if (!session.currentGid) return;
|
||||
await createSubgroup({
|
||||
SubGroupName: newDeptName.trim(),
|
||||
SubGroupName: name,
|
||||
Gid: session.currentGid,
|
||||
IsAdminGroup: false,
|
||||
CacheTriggerMeters: parseInt(cacheTriggerMeters) || 20,
|
||||
CacheTriggerMeters: 20,
|
||||
});
|
||||
setNewDeptName('');
|
||||
await loadSubgroups();
|
||||
};
|
||||
|
||||
// Business — save per-subgroup trigger distance
|
||||
const handleSaveDeptTrigger = async (sgid) => {
|
||||
await updateSubgroup(sgid, {CacheTriggerMeters: parseInt(editDeptTrigger) || 20});
|
||||
setEditingSGid(null);
|
||||
await loadSubgroups();
|
||||
await refreshSettings();
|
||||
};
|
||||
|
||||
// Business — delete a department
|
||||
@@ -142,12 +206,21 @@ const GameSettingsScreen = () => {
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
await deleteSubgroup(sg.SGid);
|
||||
await loadSubgroups();
|
||||
await refreshSettings();
|
||||
},
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
// Business — navigate to department settings
|
||||
const handleEnterDepartment = (sg) => {
|
||||
navigation.navigate('DepartmentSettingsScreen', {
|
||||
SGid: sg.SGid,
|
||||
Gid: session.currentGid,
|
||||
SubGroupName: sg.SubGroupName,
|
||||
});
|
||||
};
|
||||
|
||||
// View -----------------------
|
||||
|
||||
if (loading) {
|
||||
@@ -169,6 +242,144 @@ const GameSettingsScreen = () => {
|
||||
// Filter subgroups for the department list (non-admin only)
|
||||
const departments = subgroupList.filter((sg) => !sg.IsAdminGroup);
|
||||
|
||||
// ===== Business Admin View =====
|
||||
if (isBusinessGroup) {
|
||||
return (
|
||||
<Screen>
|
||||
<ScrollView contentContainerStyle={styles.formContainer} showsVerticalScrollIndicator={false}>
|
||||
<Text style={styles.sectionTitle}>Game Configuration :</Text>
|
||||
|
||||
<Text style={styles.label}>Organisation Name:</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={businessName}
|
||||
onChangeText={setBusinessName}
|
||||
placeholder="Enter organisation name"
|
||||
placeholderTextColor="#9ca3af"
|
||||
/>
|
||||
|
||||
<View style={styles.codeSection}>
|
||||
<Text style={styles.codeLabel}>Admin join code:</Text>
|
||||
<Text style={styles.codeValue}>{adminJoinCode || '—'}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.codeSection}>
|
||||
<Text style={styles.codeLabel}>Org join code:</Text>
|
||||
<Text style={styles.codeValue}>{orgJoinCode || '—'}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.saveWrap}>
|
||||
<ButtonTray>
|
||||
<Button
|
||||
label={saving ? 'Saving...' : 'Save Settings'}
|
||||
onClick={handleSave}
|
||||
styleButton={styles.saveButton}
|
||||
styleLabel={styles.saveLabel}
|
||||
/>
|
||||
</ButtonTray>
|
||||
</View>
|
||||
|
||||
{/* Departments List */}
|
||||
<View style={styles.deptSection}>
|
||||
<Text style={styles.sectionTitle}>Departments:</Text>
|
||||
{departments.map((sg, index) => (
|
||||
<Card key={sg.SGid}>
|
||||
<Text style={styles.deptIndexLabel}>Department {index + 1}:</Text>
|
||||
<View style={styles.deptCardRow}>
|
||||
<View style={styles.deptCardInfo}>
|
||||
<Text style={styles.deptInfoLabel}>Department Name</Text>
|
||||
<Text style={styles.deptName}>{sg.SubGroupName}</Text>
|
||||
</View>
|
||||
<View style={styles.deptCardActions}>
|
||||
<Button
|
||||
label="Department Game"
|
||||
onClick={() => handleEnterDepartment(sg)}
|
||||
styleButton={styles.deptOpenButton}
|
||||
styleLabel={styles.deptBtnLabel}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.deptCardRow}>
|
||||
<View style={styles.deptCardInfo}>
|
||||
<Text style={styles.deptInfoLabel}>Department Join code</Text>
|
||||
<Text style={styles.deptCode}>{sg.JoinCode || '—'}</Text>
|
||||
</View>
|
||||
<View style={styles.deptCardActions}>
|
||||
<Button
|
||||
label="Delete Department"
|
||||
onClick={() => handleDeleteDepartment(sg)}
|
||||
styleButton={styles.deptDeleteButton}
|
||||
styleLabel={styles.deptBtnLabel}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</Card>
|
||||
))}
|
||||
{departments.length === 0 && (
|
||||
<Text style={styles.emptyText}>No departments yet.</Text>
|
||||
)}
|
||||
<View style={styles.createDeptRow}>
|
||||
<TextInput
|
||||
style={[styles.input, {flex: 1}]}
|
||||
value={newDeptName}
|
||||
onChangeText={setNewDeptName}
|
||||
placeholder="New department name"
|
||||
placeholderTextColor="#9ca3af"
|
||||
/>
|
||||
<Button
|
||||
label="Add"
|
||||
onClick={handleCreateDepartment}
|
||||
styleButton={styles.createDeptButton}
|
||||
styleLabel={styles.createDeptLabel}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Admin Waitlist */}
|
||||
{waitlist.length > 0 && (
|
||||
<View style={styles.waitlistSection}>
|
||||
<Text style={styles.sectionTitle}>Admin Waitlist</Text>
|
||||
{waitlist.map((entry) => (
|
||||
<Card key={entry.id}>
|
||||
<View style={styles.waitlistRow}>
|
||||
<Text style={styles.waitlistName}>
|
||||
{waitlistNames[entry.Uid] || `User ${entry.Uid}`}
|
||||
</Text>
|
||||
<View style={styles.waitlistActions}>
|
||||
<Button
|
||||
label="Approve"
|
||||
onClick={() => handleApproveAdmin(entry)}
|
||||
styleButton={styles.approveButton}
|
||||
styleLabel={styles.approveBtnLabel}
|
||||
/>
|
||||
<Button
|
||||
label="Reject"
|
||||
onClick={() => handleRejectAdmin(entry)}
|
||||
styleButton={styles.rejectButton}
|
||||
styleLabel={styles.rejectBtnLabel}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</Card>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Reset Entire Game */}
|
||||
<View style={styles.resetSection}>
|
||||
<Button
|
||||
label="Reset Entire Game"
|
||||
onClick={handleResetGame}
|
||||
styleButton={styles.resetButton}
|
||||
styleLabel={styles.resetLabel}
|
||||
/>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</Screen>
|
||||
);
|
||||
}
|
||||
|
||||
// ===== Individual Admin View =====
|
||||
return (
|
||||
<Screen>
|
||||
<ScrollView contentContainerStyle={styles.formContainer} showsVerticalScrollIndicator={false}>
|
||||
@@ -209,7 +420,7 @@ const GameSettingsScreen = () => {
|
||||
</View>
|
||||
|
||||
<View style={styles.codeSection}>
|
||||
<Text style={styles.codeLabel}>Player Join Code</Text>
|
||||
<Text style={styles.codeLabel}>Game Join Code</Text>
|
||||
<Text style={styles.codeValue}>{memberJoinCode || '—'}</Text>
|
||||
</View>
|
||||
|
||||
@@ -224,83 +435,6 @@ const GameSettingsScreen = () => {
|
||||
</ButtonTray>
|
||||
</View>
|
||||
|
||||
{/* Business: Departments / Subgroups management */}
|
||||
{isBusiness && (
|
||||
<View style={styles.deptSection}>
|
||||
<Text style={styles.sectionTitle}>Departments</Text>
|
||||
{departments.map((sg) => (
|
||||
<Card key={sg.SGid}>
|
||||
<View style={styles.deptRow}>
|
||||
<View style={styles.deptInfo}>
|
||||
<Text style={styles.deptName}>{sg.SubGroupName}</Text>
|
||||
<Text style={styles.deptCode}>Join Code: {sg.JoinCode || '—'}</Text>
|
||||
<Text style={styles.deptTrigger}>Claim Distance: {sg.CacheTriggerMeters || 20}m</Text>
|
||||
</View>
|
||||
<View style={styles.deptActions}>
|
||||
<Button
|
||||
label="Edit"
|
||||
onClick={() => { setEditingSGid(sg.SGid); setEditDeptTrigger(String(sg.CacheTriggerMeters || 20)); }}
|
||||
styleButton={styles.deptEditButton}
|
||||
styleLabel={styles.deptBtnLabel}
|
||||
/>
|
||||
<Button
|
||||
label="Delete"
|
||||
onClick={() => handleDeleteDepartment(sg)}
|
||||
styleButton={styles.deptDeleteButton}
|
||||
styleLabel={styles.deptBtnLabel}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
{editingSGid === sg.SGid && (
|
||||
<View style={styles.deptEditRow}>
|
||||
<Text style={styles.label}>Claim Distance (metres)</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={editDeptTrigger}
|
||||
onChangeText={setEditDeptTrigger}
|
||||
keyboardType="numeric"
|
||||
placeholder="20"
|
||||
placeholderTextColor="#9ca3af"
|
||||
/>
|
||||
<ButtonTray>
|
||||
<Button
|
||||
label="Save"
|
||||
onClick={() => handleSaveDeptTrigger(sg.SGid)}
|
||||
styleButton={styles.deptSaveButton}
|
||||
styleLabel={styles.deptBtnLabel}
|
||||
/>
|
||||
<Button
|
||||
label="Cancel"
|
||||
onClick={() => setEditingSGid(null)}
|
||||
styleButton={styles.deptCancelButton}
|
||||
styleLabel={styles.deptBtnLabel}
|
||||
/>
|
||||
</ButtonTray>
|
||||
</View>
|
||||
)}
|
||||
</Card>
|
||||
))}
|
||||
{departments.length === 0 && (
|
||||
<Text style={styles.emptyText}>No departments yet.</Text>
|
||||
)}
|
||||
<View style={styles.createDeptRow}>
|
||||
<TextInput
|
||||
style={[styles.input, {flex: 1}]}
|
||||
value={newDeptName}
|
||||
onChangeText={setNewDeptName}
|
||||
placeholder="New department name"
|
||||
placeholderTextColor="#9ca3af"
|
||||
/>
|
||||
<Button
|
||||
label="Add"
|
||||
onClick={handleCreateDepartment}
|
||||
styleButton={styles.createDeptButton}
|
||||
styleLabel={styles.createDeptLabel}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Admin Waitlist */}
|
||||
{waitlist.length > 0 && (
|
||||
<View style={styles.waitlistSection}>
|
||||
@@ -348,18 +482,18 @@ const GameSettingsScreen = () => {
|
||||
const styles = StyleSheet.create({
|
||||
center: {justifyContent: 'center', alignItems: 'center'},
|
||||
formContainer: {paddingBottom: 20},
|
||||
sectionTitle: {fontSize: 20, fontWeight: '700', color: '#1f2937', marginBottom: 16},
|
||||
label: {fontSize: 14, fontWeight: '600', color: '#374151', marginBottom: 6, marginTop: 14},
|
||||
body: {color: '#4b5563', fontSize: 15},
|
||||
sectionTitle: {fontSize: 20, fontWeight: '700', color: '#cdd6f4', marginBottom: 16},
|
||||
label: {fontSize: 14, fontWeight: '600', color: '#bac2de', marginBottom: 6, marginTop: 14},
|
||||
body: {color: '#bac2de', fontSize: 15},
|
||||
input: {
|
||||
borderWidth: 1,
|
||||
borderColor: '#d1d5db',
|
||||
borderColor: '#45475a',
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 10,
|
||||
fontSize: 16,
|
||||
color: '#1f2937',
|
||||
backgroundColor: '#ffffff',
|
||||
color: '#cdd6f4',
|
||||
backgroundColor: '#313244',
|
||||
},
|
||||
toggleRow: {
|
||||
flexDirection: 'row',
|
||||
@@ -368,9 +502,9 @@ const styles = StyleSheet.create({
|
||||
marginTop: 16,
|
||||
paddingVertical: 10,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#e5e7eb',
|
||||
borderBottomColor: '#45475a',
|
||||
},
|
||||
toggleLabel: {fontSize: 14, fontWeight: '600', color: '#374151'},
|
||||
toggleLabel: {fontSize: 14, fontWeight: '600', color: '#bac2de'},
|
||||
codeSection: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
@@ -378,48 +512,43 @@ const styles = StyleSheet.create({
|
||||
marginTop: 16,
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 12,
|
||||
backgroundColor: '#f3f4f6',
|
||||
backgroundColor: '#313244',
|
||||
borderRadius: 8,
|
||||
},
|
||||
codeLabel: {fontSize: 14, fontWeight: '600', color: '#374151'},
|
||||
codeValue: {fontSize: 16, fontWeight: '700', color: '#2563eb', letterSpacing: 2},
|
||||
codeLabel: {fontSize: 14, fontWeight: '600', color: '#bac2de'},
|
||||
codeValue: {fontSize: 16, fontWeight: '700', color: '#bd93f9', letterSpacing: 2},
|
||||
saveWrap: {marginTop: 24},
|
||||
saveButton: {backgroundColor: '#16a34a', borderColor: '#16a34a'},
|
||||
saveLabel: {color: '#ffffff', fontWeight: '600'},
|
||||
// Department / subgroup styles
|
||||
saveButton: {backgroundColor: '#a6e3a1', borderColor: '#a6e3a1'},
|
||||
saveLabel: {color: '#1e1e2e', fontWeight: '600'},
|
||||
deptSection: {marginTop: 30},
|
||||
deptRow: {flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start'},
|
||||
deptInfo: {flex: 1},
|
||||
deptName: {fontSize: 15, fontWeight: '700', color: '#1f2937'},
|
||||
deptCode: {fontSize: 13, fontWeight: '600', color: '#2563eb', marginTop: 2, letterSpacing: 1},
|
||||
deptTrigger: {fontSize: 12, color: '#6b7280', marginTop: 2},
|
||||
deptActions: {flexDirection: 'row', gap: 6},
|
||||
deptEditButton: {backgroundColor: '#2563eb', borderColor: '#2563eb', minHeight: 36, flex: 0, paddingHorizontal: 10},
|
||||
deptDeleteButton: {backgroundColor: '#dc2626', borderColor: '#dc2626', minHeight: 36, flex: 0, paddingHorizontal: 10},
|
||||
deptBtnLabel: {color: '#ffffff', fontWeight: '600', fontSize: 13},
|
||||
deptEditRow: {marginTop: 10, paddingTop: 10, borderTopWidth: 1, borderTopColor: '#e5e7eb'},
|
||||
deptSaveButton: {backgroundColor: '#16a34a', borderColor: '#16a34a'},
|
||||
deptCancelButton: {backgroundColor: '#6b7280', borderColor: '#6b7280'},
|
||||
deptIndexLabel: {fontSize: 14, fontWeight: '700', color: '#bac2de', marginBottom: 8},
|
||||
deptCardRow: {flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8},
|
||||
deptCardInfo: {flex: 1, paddingRight: 10},
|
||||
deptCardActions: {flexDirection: 'row', gap: 6, flex: 0},
|
||||
deptInfoLabel: {fontSize: 12, fontWeight: '600', color: '#6c7086', marginBottom: 2},
|
||||
deptName: {fontSize: 15, fontWeight: '700', color: '#cdd6f4'},
|
||||
deptCode: {fontSize: 13, fontWeight: '600', color: '#bd93f9', marginTop: 2, letterSpacing: 1},
|
||||
deptDeleteButton: {backgroundColor: '#D92800', borderColor: '#D92800', minHeight: 36, flex: 0, paddingHorizontal: 10},
|
||||
deptOpenButton: {backgroundColor: '#bd93f9', borderColor: '#bd93f9', minHeight: 36, flex: 0, paddingHorizontal: 10},
|
||||
deptBtnLabel: {color: '#1e1e2e', fontWeight: '600', fontSize: 13},
|
||||
createDeptRow: {flexDirection: 'row', gap: 10, marginTop: 12, alignItems: 'center'},
|
||||
createDeptButton: {backgroundColor: '#16a34a', borderColor: '#16a34a', flex: 0, paddingHorizontal: 16},
|
||||
createDeptLabel: {color: '#ffffff', fontWeight: '600'},
|
||||
emptyText: {color: '#9ca3af', textAlign: 'center', marginTop: 10, fontSize: 14},
|
||||
// Admin waitlist styles
|
||||
createDeptButton: {backgroundColor: '#a6e3a1', borderColor: '#a6e3a1', flex: 0, paddingHorizontal: 16},
|
||||
createDeptLabel: {color: '#1e1e2e', fontWeight: '600'},
|
||||
emptyText: {color: '#6c7086', textAlign: 'center', marginTop: 10, fontSize: 14},
|
||||
waitlistSection: {marginTop: 30},
|
||||
waitlistRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
},
|
||||
waitlistName: {fontSize: 15, fontWeight: '600', color: '#1f2937', flex: 1},
|
||||
waitlistName: {fontSize: 15, fontWeight: '600', color: '#cdd6f4', flex: 1},
|
||||
waitlistActions: {flexDirection: 'row', gap: 6},
|
||||
approveButton: {backgroundColor: '#16a34a', borderColor: '#16a34a', minHeight: 36, flex: 0, paddingHorizontal: 10},
|
||||
approveBtnLabel: {color: '#ffffff', fontWeight: '600', fontSize: 13},
|
||||
rejectButton: {backgroundColor: '#dc2626', borderColor: '#dc2626', minHeight: 36, flex: 0, paddingHorizontal: 10},
|
||||
rejectBtnLabel: {color: '#ffffff', fontWeight: '600', fontSize: 13},
|
||||
// Reset game styles
|
||||
approveButton: {backgroundColor: '#a6e3a1', borderColor: '#a6e3a1', minHeight: 36, flex: 0, paddingHorizontal: 10},
|
||||
approveBtnLabel: {color: '#1e1e2e', fontWeight: '600', fontSize: 13},
|
||||
rejectButton: {backgroundColor: '#D92800', borderColor: '#D92800', minHeight: 36, flex: 0, paddingHorizontal: 10},
|
||||
rejectBtnLabel: {color: '#1e1e2e', fontWeight: '600', fontSize: 13},
|
||||
resetSection: {marginTop: 30},
|
||||
resetButton: {backgroundColor: '#dc2626', borderColor: '#dc2626'},
|
||||
resetButton: {backgroundColor: '#D92800', borderColor: '#D92800'},
|
||||
resetLabel: {color: '#ffffff', fontWeight: '600'},
|
||||
});
|
||||
|
||||
|
||||
@@ -6,12 +6,14 @@ import {Button} from '../UI/Button';
|
||||
import useGameHook from '../../hooks/useGameHook';
|
||||
import {getSession} from '../../hooks/SessionStore';
|
||||
|
||||
const LeaderboardScreen = () => {
|
||||
const LeaderboardScreen = ({navigation, route}) => {
|
||||
// Initialisation ------------
|
||||
|
||||
const session = getSession();
|
||||
const isAdmin = session.isAcceptedAdmin;
|
||||
const {getLobby, getTeams, getTeamMembers, getGroupMembers, getCaches, getUser, deleteTeam, resetPlayerProgress} = useGameHook();
|
||||
const isBusiness = Boolean(session.isBusiness);
|
||||
const selectedDepartment = route?.params?.department || null;
|
||||
const {getLobby, getSubgroup, getTeams, getTeamMembers, getGroupMembers, getCaches, getUser, deleteTeam, resetPlayerProgress, resetTeamProgress, getSubgroups} = useGameHook();
|
||||
|
||||
// State ----------------------
|
||||
|
||||
@@ -23,6 +25,7 @@ const LeaderboardScreen = () => {
|
||||
const [myTeamRanking, setMyTeamRanking] = useState([]);
|
||||
const [expandedTeam, setExpandedTeam] = useState(null);
|
||||
const [totalCacheCount, setTotalCacheCount] = useState(0);
|
||||
const [departmentList, setDepartmentList] = useState([]);
|
||||
|
||||
// Handlers -------------------
|
||||
|
||||
@@ -30,16 +33,32 @@ const LeaderboardScreen = () => {
|
||||
if (!session.currentGid) { setLoading(false); return; }
|
||||
|
||||
const group = await getLobby(session.currentGid);
|
||||
const isTeams = Boolean(group?.TeamsEnabled);
|
||||
|
||||
const effectiveSGid = isBusiness
|
||||
? ((isAdmin && selectedDepartment?.SGid) ? selectedDepartment.SGid : (isAdmin ? null : session.currentSGid))
|
||||
: null;
|
||||
|
||||
let isTeams = Boolean(group?.TeamsEnabled);
|
||||
if (effectiveSGid !== null && effectiveSGid !== undefined) {
|
||||
const subgroup = await getSubgroup(effectiveSGid);
|
||||
if (subgroup) isTeams = Boolean(subgroup.TeamsEnabled);
|
||||
}
|
||||
setTeamsEnabled(isTeams);
|
||||
|
||||
const caches = await getCaches(session.currentGid, null);
|
||||
if (isBusiness && isAdmin && !selectedDepartment) {
|
||||
const sgs = await getSubgroups(session.currentGid);
|
||||
setDepartmentList((sgs || []).filter((sg) => !sg.IsAdminGroup));
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const caches = await getCaches(session.currentGid, effectiveSGid ?? null);
|
||||
const total = (caches || []).length;
|
||||
setTotalCacheCount(total);
|
||||
|
||||
if (isTeams) {
|
||||
// Build team rankings
|
||||
const teams = await getTeams(session.currentGid);
|
||||
const teams = await getTeams(session.currentGid, effectiveSGid ?? null);
|
||||
const teamData = [];
|
||||
|
||||
for (const team of (teams || [])) {
|
||||
@@ -80,9 +99,9 @@ const LeaderboardScreen = () => {
|
||||
}
|
||||
} else {
|
||||
// Build player rankings (no teams)
|
||||
const allMembers = await getGroupMembers(session.currentGid);
|
||||
const scopedMembers = await getGroupMembers(session.currentGid, effectiveSGid ?? null);
|
||||
const playerData = [];
|
||||
for (const m of (allMembers || []).filter((mem) => !mem.IsAcceptedAdmin)) {
|
||||
for (const m of (scopedMembers || []).filter((mem) => !mem.IsAcceptedAdmin)) {
|
||||
const user = await getUser(m.Uid);
|
||||
const count = (caches || []).filter((c) =>
|
||||
(c.Claims || []).some((cl) => cl.Uid === m.Uid)
|
||||
@@ -98,7 +117,7 @@ const LeaderboardScreen = () => {
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
}, [session.currentGid, session.currentTid]);
|
||||
}, [session.currentGid, session.currentTid, session.currentSGid, isBusiness, isAdmin, selectedDepartment, selectedDepartment?.SGid]);
|
||||
|
||||
useEffect(() => { loadData(); }, [loadData]);
|
||||
|
||||
@@ -133,6 +152,21 @@ const LeaderboardScreen = () => {
|
||||
]);
|
||||
};
|
||||
|
||||
const handleResetTeam = (team) => {
|
||||
Alert.alert('Reset Team Progress', `Reset all cache claims for team "${team.name}"?`, [
|
||||
{text: 'Cancel', style: 'cancel'},
|
||||
{
|
||||
text: 'Reset',
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
await resetTeamProgress(session.currentGid, team.tid);
|
||||
Alert.alert('Done', `Progress for team "${team.name}" has been reset.`);
|
||||
await loadData();
|
||||
},
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
// View -----------------------
|
||||
|
||||
if (loading) {
|
||||
@@ -151,6 +185,32 @@ const LeaderboardScreen = () => {
|
||||
);
|
||||
}
|
||||
|
||||
if (isBusiness && isAdmin && !selectedDepartment) {
|
||||
return (
|
||||
<Screen style={styles.container}>
|
||||
<Text style={styles.departmentTitle}>Departments</Text>
|
||||
<ScrollView style={styles.listSection}>
|
||||
{departmentList.map((department) => (
|
||||
<Pressable
|
||||
key={department.SGid}
|
||||
onPress={() => navigation.navigate('LeaderboardScreen', {department})}
|
||||
>
|
||||
<Card>
|
||||
<View style={styles.rankRow}>
|
||||
<Text style={styles.rankName}>{department.SubGroupName}</Text>
|
||||
<Text style={styles.openLabel}>Open</Text>
|
||||
</View>
|
||||
</Card>
|
||||
</Pressable>
|
||||
))}
|
||||
{departmentList.length === 0 && (
|
||||
<Text style={styles.emptyText}>No departments yet.</Text>
|
||||
)}
|
||||
</ScrollView>
|
||||
</Screen>
|
||||
);
|
||||
}
|
||||
|
||||
// Helper: render an expandable team card (admin version with delete + reset)
|
||||
const renderAdminTeamCard = (team, index) => (
|
||||
<Card key={team.tid}>
|
||||
@@ -179,11 +239,17 @@ const LeaderboardScreen = () => {
|
||||
</View>
|
||||
))}
|
||||
<View style={styles.deleteTeamWrap}>
|
||||
<Button
|
||||
label="Reset Team Progress"
|
||||
onClick={() => handleResetTeam(team)}
|
||||
styleButton={styles.resetTeamButton}
|
||||
styleLabel={styles.deleteTeamLabel}
|
||||
/>
|
||||
<Button
|
||||
label="Delete Team"
|
||||
onClick={() => handleDeleteTeam(team)}
|
||||
styleButton={styles.deleteTeamButton}
|
||||
styleLabel={styles.deleteTeamLabel}
|
||||
styleLabel={styles.deleteTeamDeleteLabel}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
@@ -248,7 +314,10 @@ const LeaderboardScreen = () => {
|
||||
// Teams enabled: show team list (no tabs for admin)
|
||||
if (teamsEnabled) {
|
||||
return (
|
||||
<Screen style={styles.container}>
|
||||
<Screen style={styles.container} showBack={Boolean(selectedDepartment)}>
|
||||
{selectedDepartment && (
|
||||
<Text style={styles.departmentTitle}>{selectedDepartment.SubGroupName}</Text>
|
||||
)}
|
||||
<ScrollView style={styles.listSection}>
|
||||
{teamRankings.map((team, index) => renderAdminTeamCard(team, index))}
|
||||
{teamRankings.length === 0 && (
|
||||
@@ -261,7 +330,10 @@ const LeaderboardScreen = () => {
|
||||
|
||||
// Teams disabled: show player list with reset buttons
|
||||
return (
|
||||
<Screen style={styles.container}>
|
||||
<Screen style={styles.container} showBack={Boolean(selectedDepartment)}>
|
||||
{selectedDepartment && (
|
||||
<Text style={styles.departmentTitle}>{selectedDepartment.SubGroupName}</Text>
|
||||
)}
|
||||
<ScrollView style={styles.listSection}>
|
||||
{playerRankings.map((player, index) => renderAdminPlayerCard(player, index))}
|
||||
{playerRankings.length === 0 && (
|
||||
@@ -339,11 +411,13 @@ const LeaderboardScreen = () => {
|
||||
const styles = StyleSheet.create({
|
||||
center: {justifyContent: 'center', alignItems: 'center'},
|
||||
container: {padding: 0},
|
||||
body: {color: '#4b5563', fontSize: 15},
|
||||
body: {color: '#bac2de', fontSize: 15},
|
||||
departmentTitle: {fontSize: 20, fontWeight: '700', color: '#cdd6f4', paddingHorizontal: 15, paddingTop: 15, marginBottom: 6},
|
||||
openLabel: {fontSize: 13, fontWeight: '700', color: '#bd93f9'},
|
||||
tabRow: {
|
||||
flexDirection: 'row',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#e5e7eb',
|
||||
borderBottomColor: '#45475a',
|
||||
},
|
||||
tabButton: {
|
||||
flex: 1,
|
||||
@@ -352,46 +426,45 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
tabButtonActive: {
|
||||
borderBottomWidth: 2,
|
||||
borderBottomColor: '#2563eb',
|
||||
borderBottomColor: '#bd93f9',
|
||||
},
|
||||
tabLabel: {fontSize: 15, fontWeight: '600', color: '#6b7280'},
|
||||
tabLabelActive: {color: '#2563eb'},
|
||||
tabLabel: {fontSize: 15, fontWeight: '600', color: '#6c7086'},
|
||||
tabLabelActive: {color: '#bd93f9'},
|
||||
listSection: {flex: 1, paddingHorizontal: 15, paddingTop: 10},
|
||||
rankRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
},
|
||||
rank: {fontSize: 16, fontWeight: '700', color: '#374151', width: 32},
|
||||
rankName: {flex: 1, fontSize: 15, fontWeight: '600', color: '#1f2937'},
|
||||
score: {fontSize: 14, fontWeight: '600', color: '#16a34a'},
|
||||
rank: {fontSize: 16, fontWeight: '700', color: '#bac2de', width: 32},
|
||||
rankName: {flex: 1, fontSize: 15, fontWeight: '600', color: '#cdd6f4'},
|
||||
score: {fontSize: 14, fontWeight: '600', color: '#a6e3a1'},
|
||||
subRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingLeft: 42,
|
||||
paddingVertical: 6,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: '#f3f4f6',
|
||||
borderTopColor: '#45475a',
|
||||
},
|
||||
subName: {fontSize: 13, color: '#4b5563'},
|
||||
subScore: {fontSize: 13, fontWeight: '600', color: '#16a34a', marginRight: 8},
|
||||
emptyText: {color: '#9ca3af', textAlign: 'center', marginTop: 30, fontSize: 14},
|
||||
// Admin delete team button
|
||||
deleteTeamWrap: {marginTop: 8, paddingLeft: 42},
|
||||
deleteTeamButton: {backgroundColor: '#dc2626', borderColor: '#dc2626', minHeight: 36},
|
||||
deleteTeamLabel: {color: '#ffffff', fontWeight: '600', fontSize: 13},
|
||||
// Admin reset mini button (inline)
|
||||
subName: {fontSize: 13, color: '#bac2de'},
|
||||
subScore: {fontSize: 13, fontWeight: '600', color: '#a6e3a1', marginRight: 8},
|
||||
emptyText: {color: '#6c7086', textAlign: 'center', marginTop: 30, fontSize: 14},
|
||||
deleteTeamWrap: {marginTop: 8, paddingLeft: 42, flexDirection: 'row', gap: 8},
|
||||
resetTeamButton: {backgroundColor: '#f4a460', borderColor: '#f4a460', minHeight: 36, flex: 1},
|
||||
deleteTeamButton: {backgroundColor: '#D92800', borderColor: '#D92800', minHeight: 36, flex: 1},
|
||||
deleteTeamLabel: {color: '#1e1e2e', fontWeight: '600', fontSize: 13},
|
||||
deleteTeamDeleteLabel: {color: '#ffffff', fontWeight: '600', fontSize: 13},
|
||||
resetMiniButton: {
|
||||
backgroundColor: '#f59e0b',
|
||||
backgroundColor: '#86efac',
|
||||
borderRadius: 6,
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 4,
|
||||
marginLeft: 6,
|
||||
},
|
||||
resetMiniLabel: {color: '#ffffff', fontWeight: '600', fontSize: 12},
|
||||
// Current user self-highlight (light green card / sub-row)
|
||||
selfCard: {backgroundColor: '#dcfce7', borderColor: '#86efac'},
|
||||
selfSubRow: {backgroundColor: '#dcfce7'},
|
||||
resetMiniLabel: {color: '#1e1e2e', fontWeight: '600', fontSize: 12},
|
||||
selfCard: {backgroundColor: 'rgba(166, 227, 161, 0.15)', borderColor: '#a6e3a1'},
|
||||
selfSubRow: {backgroundColor: 'rgba(166, 227, 161, 0.15)'},
|
||||
});
|
||||
|
||||
export default LeaderboardScreen;
|
||||
@@ -5,16 +5,9 @@ import Screen from '../layout/Screen';
|
||||
import {Button, ButtonTray} from '../UI/Button';
|
||||
|
||||
const LoginScreen = ({ navigation }) => {
|
||||
// Initialisation -------------
|
||||
|
||||
const { email, password, error, isLoading, setEmail, setPassword, login } = useAuthHook();
|
||||
|
||||
// State ----------------------
|
||||
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
// Handlers -------------------
|
||||
|
||||
const handleLogin = async () => {
|
||||
setSubmitting(true);
|
||||
const user = await login();
|
||||
@@ -29,8 +22,6 @@ const LoginScreen = ({ navigation }) => {
|
||||
navigation.reset({ index: 0, routes: [{ name: 'Game' }] });
|
||||
};
|
||||
|
||||
// View -----------------------
|
||||
|
||||
return (
|
||||
<Screen style={styles.container}>
|
||||
<View style={styles.content}>
|
||||
@@ -39,7 +30,7 @@ const LoginScreen = ({ navigation }) => {
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Email"
|
||||
placeholderTextColor="#9ca3af"
|
||||
placeholderTextColor="#6c7086"
|
||||
autoCapitalize="none"
|
||||
keyboardType="email-address"
|
||||
value={email}
|
||||
@@ -49,7 +40,7 @@ const LoginScreen = ({ navigation }) => {
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Password"
|
||||
placeholderTextColor="#9ca3af"
|
||||
placeholderTextColor="#6c7086"
|
||||
secureTextEntry
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
@@ -87,33 +78,34 @@ const styles = StyleSheet.create({
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'center',
|
||||
marginBottom: 10,
|
||||
color: '#cdd6f4',
|
||||
},
|
||||
input: {
|
||||
borderWidth: 1,
|
||||
borderColor: '#d1d5db',
|
||||
borderColor: '#45475a',
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 10,
|
||||
fontSize: 16,
|
||||
color: '#1f2937',
|
||||
backgroundColor: '#ffffff',
|
||||
color: '#cdd6f4',
|
||||
backgroundColor: '#313244',
|
||||
},
|
||||
errorText: {
|
||||
color: '#dc2626',
|
||||
color: '#D92800',
|
||||
textAlign: 'center',
|
||||
marginTop: 5,
|
||||
},
|
||||
submitButton: {
|
||||
backgroundColor: '#2563eb',
|
||||
borderColor: '#2563eb',
|
||||
backgroundColor: '#bd93f9',
|
||||
borderColor: '#bd93f9',
|
||||
},
|
||||
submitText: {
|
||||
color: '#ffffff',
|
||||
color: '#1e1e2e',
|
||||
fontWeight: '600',
|
||||
},
|
||||
linkText: {
|
||||
textAlign: 'center',
|
||||
color: '#2563eb',
|
||||
color: '#bd93f9',
|
||||
fontWeight: '600',
|
||||
paddingVertical: 10,
|
||||
},
|
||||
|
||||
@@ -16,54 +16,60 @@ import {getFovCone} from '../../utils/geoMath';
|
||||
const DEFAULT_REGION = {latitude: 51.5074, longitude: -0.1278, latitudeDelta: 0.05, longitudeDelta: 0.05};
|
||||
|
||||
|
||||
const MapScreen = ({navigation}) => {
|
||||
// Initialisation ------------
|
||||
const MapScreen = ({navigation, route}) => {
|
||||
const session = getSession();
|
||||
const {getCaches, claimCache, upsertCache, deleteCache, joinPrivateGame, createPrivateGame, getLobby, getUser, getSubgroups, getGroupByOrgCode} = useGameHook();
|
||||
|
||||
const session = getSession();
|
||||
const {getCaches, claimCache, upsertCache, deleteCache, joinPrivateGame, createPrivateGame, getLobby, getUser, getSubgroups} = useGameHook();
|
||||
// Stable ref to prevent infinite re-render loops
|
||||
const getCachesRef = useRef(getCaches);
|
||||
getCachesRef.current = getCaches;
|
||||
const lastAppliedRouteDepartmentRef = useRef(null);
|
||||
|
||||
// Stable ref to prevent infinite re-render loops
|
||||
const getCachesRef = useRef(getCaches);
|
||||
getCachesRef.current = getCaches;
|
||||
const [inGame, setInGame] = useState(Boolean(session.currentGid));
|
||||
const [isAdmin, setIsAdmin] = useState(session.isAcceptedAdmin);
|
||||
const [userLocation, setUserLocation] = useState(null);
|
||||
const [heading, setHeading] = useState(null);
|
||||
const [playerLoading, setPlayerLoading] = useState(true);
|
||||
const [cacheRecords, setCacheRecords] = useState([]);
|
||||
const [error, setError] = useState('');
|
||||
const [gameCode, setGameCode] = useState('');
|
||||
const [groupInfo, setGroupInfo] = useState(null);
|
||||
const [selectedCacheId, setSelectedCacheIdState] = useState(session.selectedCacheId);
|
||||
const [claimedPopupVisible, setClaimedPopupVisible] = useState(false);
|
||||
const [subgroups, setSubgroups] = useState([]);
|
||||
|
||||
// State ----------------------
|
||||
// Business join flow state
|
||||
const [joinStep, setJoinStep] = useState(null);
|
||||
const [orgCode, setOrgCode] = useState('');
|
||||
const [deptCode, setDeptCode] = useState('');
|
||||
const [matchedGroup, setMatchedGroup] = useState(null);
|
||||
const [joinError, setJoinError] = useState('');
|
||||
|
||||
const [inGame, setInGame] = useState(Boolean(session.currentGid));
|
||||
const [isAdmin, setIsAdmin] = useState(session.isAcceptedAdmin);
|
||||
const [userLocation, setUserLocation] = useState(null);
|
||||
const [heading, setHeading] = useState(null);
|
||||
const [playerLoading, setPlayerLoading] = useState(true);
|
||||
const [cacheRecords, setCacheRecords] = useState([]);
|
||||
const [error, setError] = useState('');
|
||||
const [gameCode, setGameCode] = useState('');
|
||||
const [groupInfo, setGroupInfo] = useState(null);
|
||||
const [selectedCacheId, setSelectedCacheIdState] = useState(session.selectedCacheId);
|
||||
const [claimedPopupVisible, setClaimedPopupVisible] = useState(false);
|
||||
const [subgroups, setSubgroups] = useState([]);
|
||||
// Admin cache creation / editing state
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [editingCacheId, setEditingCacheId] = useState(null);
|
||||
const [newCoord, setNewCoord] = useState(null);
|
||||
const [newName, setNewName] = useState('');
|
||||
const [newClue, setNewClue] = useState('');
|
||||
const [selectedCacheSubgroupId, setSelectedCacheSubgroupId] = useState(null);
|
||||
const [departmentDropdownOpen, setDepartmentDropdownOpen] = useState(false);
|
||||
|
||||
// Admin cache creation / editing state
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [editingCacheId, setEditingCacheId] = useState(null);
|
||||
const [newCoord, setNewCoord] = useState(null);
|
||||
const [newName, setNewName] = useState('');
|
||||
const [newClue, setNewClue] = useState('');
|
||||
const claimDistance = groupInfo?.CacheTriggerMeters || 20;
|
||||
const isPlayer = inGame && !isAdmin;
|
||||
const requiresTeam = Boolean(isPlayer && groupInfo?.TeamsEnabled && !session.currentTid);
|
||||
const EMPTY_CACHES = useMemo(() => [], []);
|
||||
|
||||
const claimDistance = groupInfo?.CacheTriggerMeters || 20;
|
||||
const isPlayer = inGame && !isAdmin;
|
||||
const requiresTeam = Boolean(isPlayer && groupInfo?.TeamsEnabled && !session.currentTid);
|
||||
const EMPTY_CACHES = useMemo(() => [], []);
|
||||
|
||||
// Caches not yet claimed by the current user's team (or user if no team)
|
||||
const activeCachesForPlayer = useMemo(() => {
|
||||
if (!isPlayer) return EMPTY_CACHES;
|
||||
return cacheRecords.filter((cache) => {
|
||||
const claims = cache.Claims || [];
|
||||
if (session.currentTid) {
|
||||
return !claims.some((c) => c.Tid === session.currentTid);
|
||||
}
|
||||
return !claims.some((c) => c.Uid === session.currentUid);
|
||||
});
|
||||
}, [cacheRecords, isPlayer, session.currentTid, session.currentUid, EMPTY_CACHES]);
|
||||
// Caches not yet claimed by the current user's team (or user if no team)
|
||||
const activeCachesForPlayer = useMemo(() => {
|
||||
if (!isPlayer) return EMPTY_CACHES;
|
||||
return cacheRecords.filter((cache) => {
|
||||
const claims = cache.Claims || [];
|
||||
if (session.currentTid) {
|
||||
return !claims.some((c) => c.Tid === session.currentTid);
|
||||
}
|
||||
return !claims.some((c) => c.Uid === session.currentUid);
|
||||
});
|
||||
}, [cacheRecords, isPlayer, session.currentTid, session.currentUid, EMPTY_CACHES]);
|
||||
|
||||
const {visibleCaches, isClaiming, setIsClaiming} = usePlayerGame(
|
||||
(isPlayer && !requiresTeam) ? userLocation : null,
|
||||
@@ -73,20 +79,47 @@ const MapScreen = ({navigation}) => {
|
||||
selectedCacheId,
|
||||
);
|
||||
|
||||
const mapRegion = userLocation
|
||||
? {...userLocation, latitudeDelta: 0.01, longitudeDelta: 0.01}
|
||||
: DEFAULT_REGION;
|
||||
const mapRegion = userLocation
|
||||
? {...userLocation, latitudeDelta: 0.01, longitudeDelta: 0.01}
|
||||
: DEFAULT_REGION;
|
||||
|
||||
// Handlers -------------------
|
||||
useEffect(() => {
|
||||
if (!session.currentGid) return;
|
||||
getLobby(session.currentGid).then(setGroupInfo);
|
||||
getSubgroups(session.currentGid).then(setSubgroups);
|
||||
}, [inGame]);
|
||||
|
||||
// Default member subgroup SGid for cache creation (first non-admin subgroup)
|
||||
const departments = useMemo(
|
||||
() => subgroups.filter((sg) => !sg.IsAdminGroup),
|
||||
[subgroups],
|
||||
);
|
||||
const defaultMemberSGid = departments[0]?.SGid || null;
|
||||
const departmentNamesById = useMemo(() => {
|
||||
const map = {};
|
||||
for (const subgroup of departments) {
|
||||
map[String(subgroup.SGid)] = subgroup.SubGroupName || `Department ${subgroup.SGid}`;
|
||||
}
|
||||
return map;
|
||||
}, [departments]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!session.currentGid) return;
|
||||
getLobby(session.currentGid).then(setGroupInfo);
|
||||
getSubgroups(session.currentGid).then(setSubgroups);
|
||||
}, [inGame]);
|
||||
if (!isAdmin || departments.length === 0) return;
|
||||
const routeSelectedSgid = route?.params?.selectedDepartmentSGid;
|
||||
const hasRouteDepartment = departments.some((sg) => String(sg.SGid) === String(routeSelectedSgid));
|
||||
const shouldApplyRouteDepartment =
|
||||
hasRouteDepartment && String(lastAppliedRouteDepartmentRef.current) !== String(routeSelectedSgid);
|
||||
if (shouldApplyRouteDepartment) {
|
||||
setSelectedCacheSubgroupId(routeSelectedSgid);
|
||||
lastAppliedRouteDepartmentRef.current = routeSelectedSgid;
|
||||
return;
|
||||
}
|
||||
|
||||
// Default member subgroup SGid for cache creation (first non-admin subgroup)
|
||||
const defaultMemberSGid = subgroups.find((sg) => !sg.IsAdminGroup)?.SGid || null;
|
||||
const currentStillValid = departments.some((sg) => String(sg.SGid) === String(selectedCacheSubgroupId));
|
||||
if (!currentStillValid) {
|
||||
setSelectedCacheSubgroupId(defaultMemberSGid);
|
||||
}
|
||||
}, [route?.params?.selectedDepartmentSGid, departments, isAdmin, selectedCacheSubgroupId, defaultMemberSGid]);
|
||||
|
||||
const loadCaches = useCallback(async () => {
|
||||
const currentGid = getSession().currentGid;
|
||||
@@ -190,6 +223,64 @@ const MapScreen = ({navigation}) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Business — create a new company
|
||||
const handleCreateCompany = async () => {
|
||||
const result = await createPrivateGame({
|
||||
GroupName: 'My Company',
|
||||
BusinessOrSchoolName: 'My Company',
|
||||
CreatedByUid: session.currentUid,
|
||||
TeamsEnabled: false,
|
||||
MaxMemberSubgroups: 1,
|
||||
isBusiness: true,
|
||||
});
|
||||
if (!result) return;
|
||||
const freshUser = await getUser(session.currentUid);
|
||||
if (freshUser) {
|
||||
setSessionUser(freshUser);
|
||||
setInGame(true);
|
||||
setIsAdmin(true);
|
||||
navigation.reset({index: 0, routes: [{name: 'Admin'}]});
|
||||
}
|
||||
};
|
||||
|
||||
// Business — verify the org code
|
||||
const handleVerifyOrgCode = async () => {
|
||||
setJoinError('');
|
||||
if (!orgCode.trim()) return;
|
||||
const group = await getGroupByOrgCode(orgCode.trim().toUpperCase());
|
||||
if (!group) {
|
||||
setJoinError('Organisation not found. Check your code and try again.');
|
||||
return;
|
||||
}
|
||||
setMatchedGroup(group);
|
||||
setJoinStep('deptCode');
|
||||
};
|
||||
|
||||
// Business — join a department within the matched org
|
||||
const handleJoinDepartment = async () => {
|
||||
setJoinError('');
|
||||
if (!deptCode.trim()) return;
|
||||
const result = await joinPrivateGame({
|
||||
JoinCode: deptCode.trim().toUpperCase(),
|
||||
Uid: session.currentUid,
|
||||
ExpectedGid: matchedGroup.Gid,
|
||||
});
|
||||
if (!result) {
|
||||
setJoinError('Invalid department code. Check the code and try again.');
|
||||
return;
|
||||
}
|
||||
setSessionGroup(result.Gid, result.SGid);
|
||||
const freshUser = await getUser(session.currentUid);
|
||||
if (freshUser) {
|
||||
setSessionUser(freshUser);
|
||||
setSessionTeam(freshUser.TGid ?? null);
|
||||
}
|
||||
setInGame(true);
|
||||
setIsAdmin(false);
|
||||
setJoinStep(null);
|
||||
navigation.navigate('TeamScreen');
|
||||
};
|
||||
|
||||
const handleClaim = useCallback(async (cacheId) => {
|
||||
if (!session.currentGid) return;
|
||||
const result = await claimCache({
|
||||
@@ -212,6 +303,8 @@ const MapScreen = ({navigation}) => {
|
||||
setNewCoord(fallback);
|
||||
setNewName('');
|
||||
setNewClue('');
|
||||
if (!selectedCacheSubgroupId && defaultMemberSGid) setSelectedCacheSubgroupId(defaultMemberSGid);
|
||||
setDepartmentDropdownOpen(false);
|
||||
setIsCreating(true);
|
||||
};
|
||||
|
||||
@@ -220,6 +313,8 @@ const MapScreen = ({navigation}) => {
|
||||
setNewCoord(cache.coordinates);
|
||||
setNewName(cache.name || '');
|
||||
setNewClue(cache.clue || '');
|
||||
setSelectedCacheSubgroupId(cache.subgroupId || defaultMemberSGid);
|
||||
setDepartmentDropdownOpen(false);
|
||||
setIsCreating(true);
|
||||
};
|
||||
|
||||
@@ -231,24 +326,28 @@ const MapScreen = ({navigation}) => {
|
||||
|
||||
const handleSaveCache = async () => {
|
||||
if (!newCoord || !newClue.trim() || !session.currentGid) return;
|
||||
const subgroupId = selectedCacheSubgroupId || defaultMemberSGid;
|
||||
if (!subgroupId) return;
|
||||
const payload = {
|
||||
gid: session.currentGid,
|
||||
name: newName.trim(),
|
||||
latitude: newCoord.latitude,
|
||||
longitude: newCoord.longitude,
|
||||
clue: newClue.trim(),
|
||||
subgroupId: defaultMemberSGid,
|
||||
subgroupId,
|
||||
};
|
||||
if (editingCacheId) payload.cacheId = editingCacheId;
|
||||
await upsertCache(payload);
|
||||
setIsCreating(false);
|
||||
setEditingCacheId(null);
|
||||
setDepartmentDropdownOpen(false);
|
||||
await loadCaches();
|
||||
};
|
||||
|
||||
const handleCancelCreate = () => {
|
||||
setIsCreating(false);
|
||||
setEditingCacheId(null);
|
||||
setDepartmentDropdownOpen(false);
|
||||
};
|
||||
|
||||
const handleExpandMap = () => {
|
||||
@@ -273,9 +372,86 @@ const MapScreen = ({navigation}) => {
|
||||
return claims.some((c) => c.Uid === session.currentUid);
|
||||
};
|
||||
|
||||
const showAdminDepartmentLabel = Boolean(isAdmin && groupInfo?.BusinessOrSchoolName);
|
||||
|
||||
// View -----------------------
|
||||
|
||||
// Not in a game
|
||||
// Not in a game — Business flow
|
||||
if (!inGame && session.isBusiness) {
|
||||
// Step 2: Enter department code
|
||||
if (joinStep === 'deptCode' && matchedGroup) {
|
||||
return (
|
||||
<Screen style={styles.center}>
|
||||
<Text style={styles.bizTitle}>Join {matchedGroup.BusinessOrSchoolName || 'Company'}</Text>
|
||||
<Text style={styles.bizSubtitle}>Enter your department code to join</Text>
|
||||
<View style={styles.inputRow}>
|
||||
<TextInput
|
||||
style={styles.codeInput}
|
||||
placeholder="Department Code"
|
||||
placeholderTextColor="#9ca3af"
|
||||
value={deptCode}
|
||||
onChangeText={(text) => setDeptCode(text.toUpperCase())}
|
||||
autoCapitalize="characters"
|
||||
/>
|
||||
<Button
|
||||
label="Join"
|
||||
onClick={handleJoinDepartment}
|
||||
styleButton={styles.joinButton}
|
||||
styleLabel={styles.joinLabel}
|
||||
/>
|
||||
</View>
|
||||
{joinError ? <Text style={styles.joinError}>{joinError}</Text> : null}
|
||||
<View style={styles.fullRow}>
|
||||
<ButtonTray>
|
||||
<Button
|
||||
label="Back"
|
||||
onClick={() => { setJoinStep(null); setDeptCode(''); setJoinError(''); }}
|
||||
styleButton={styles.backButton}
|
||||
styleLabel={styles.backLabel}
|
||||
/>
|
||||
</ButtonTray>
|
||||
</View>
|
||||
</Screen>
|
||||
);
|
||||
}
|
||||
|
||||
// Default: Join company or Create company choice
|
||||
return (
|
||||
<Screen style={styles.center}>
|
||||
<Text style={styles.bizTitle}>Welcome</Text>
|
||||
<Text style={styles.bizSubtitle}>Set up your organisation or join an existing one</Text>
|
||||
<View style={styles.inputRow}>
|
||||
<TextInput
|
||||
style={styles.codeInput}
|
||||
placeholder="Company Code"
|
||||
placeholderTextColor="#9ca3af"
|
||||
value={orgCode}
|
||||
onChangeText={(text) => setOrgCode(text.toUpperCase())}
|
||||
autoCapitalize="characters"
|
||||
/>
|
||||
<Button
|
||||
label="Join Company"
|
||||
onClick={handleVerifyOrgCode}
|
||||
styleButton={styles.joinButton}
|
||||
styleLabel={styles.joinLabel}
|
||||
/>
|
||||
</View>
|
||||
{joinError ? <Text style={styles.joinError}>{joinError}</Text> : null}
|
||||
<View style={styles.fullRow}>
|
||||
<ButtonTray>
|
||||
<Button
|
||||
label="Create Company"
|
||||
onClick={handleCreateCompany}
|
||||
styleButton={styles.createButton}
|
||||
styleLabel={styles.createLabel}
|
||||
/>
|
||||
</ButtonTray>
|
||||
</View>
|
||||
</Screen>
|
||||
);
|
||||
}
|
||||
|
||||
// Not in a game — Individual flow
|
||||
if (!inGame) {
|
||||
return (
|
||||
<Screen style={styles.center}>
|
||||
@@ -355,6 +531,32 @@ const MapScreen = ({navigation}) => {
|
||||
</MapView>
|
||||
</View>
|
||||
<ScrollView style={styles.formSection}>
|
||||
<Text style={styles.departmentLabel}>Department</Text>
|
||||
<Pressable
|
||||
style={styles.departmentSelector}
|
||||
onPress={() => setDepartmentDropdownOpen((prev) => !prev)}
|
||||
>
|
||||
<Text style={styles.departmentSelectorText}>
|
||||
{departmentNamesById[String(selectedCacheSubgroupId)] || 'Select department'}
|
||||
</Text>
|
||||
<Text style={styles.departmentSelectorChevron}>{departmentDropdownOpen ? '▲' : '▼'}</Text>
|
||||
</Pressable>
|
||||
{departmentDropdownOpen && (
|
||||
<View style={styles.departmentMenu}>
|
||||
{departments.map((department) => (
|
||||
<Pressable
|
||||
key={department.SGid}
|
||||
style={styles.departmentOption}
|
||||
onPress={() => {
|
||||
setSelectedCacheSubgroupId(department.SGid);
|
||||
setDepartmentDropdownOpen(false);
|
||||
}}
|
||||
>
|
||||
<Text style={styles.departmentOptionText}>{department.SubGroupName}</Text>
|
||||
</Pressable>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
<TextInput
|
||||
style={styles.formInput}
|
||||
placeholder="Cache Name"
|
||||
@@ -462,6 +664,7 @@ const MapScreen = ({navigation}) => {
|
||||
key={cache.id}
|
||||
cache={cache}
|
||||
isAdmin={true}
|
||||
departmentName={showAdminDepartmentLabel ? (departmentNamesById[String(cache.subgroupId)] || 'Unassigned') : null}
|
||||
onEdit={handleEditCache}
|
||||
onDelete={handleDeleteCache}
|
||||
/>
|
||||
@@ -548,27 +751,31 @@ const styles = StyleSheet.create({
|
||||
center: {justifyContent: 'center', alignItems: 'center'},
|
||||
containerMap: {padding: 0},
|
||||
mapContainer: {height: 200},
|
||||
error: {color: '#dc2626', fontSize: 15},
|
||||
loadingText: {color: '#6b7280', fontSize: 14, marginTop: 10},
|
||||
error: {color: '#D92800', fontSize: 15},
|
||||
loadingText: {color: '#6c7086', fontSize: 14, marginTop: 10},
|
||||
inputRow: {flexDirection: 'row', gap: 10, marginBottom: 15, width: '100%', paddingHorizontal: 20},
|
||||
fullRow: {width: '100%', paddingHorizontal: 20},
|
||||
codeInput: {
|
||||
flex: 1,
|
||||
borderWidth: 1,
|
||||
borderColor: '#d1d5db',
|
||||
borderColor: '#45475a',
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 10,
|
||||
fontSize: 16,
|
||||
color: '#1f2937',
|
||||
backgroundColor: '#ffffff',
|
||||
},
|
||||
joinButton: {backgroundColor: '#2563eb', borderColor: '#2563eb', flex: 0, paddingHorizontal: 20},
|
||||
joinLabel: {color: '#ffffff', fontWeight: '600'},
|
||||
createButton: {backgroundColor: '#16a34a', borderColor: '#16a34a'},
|
||||
createLabel: {color: '#ffffff', fontWeight: '600'},
|
||||
color: '#cdd6f4',
|
||||
backgroundColor: '#313244',
|
||||
}, joinButton: {backgroundColor: '#bd93f9', borderColor: '#bd93f9', flex: 0, paddingHorizontal: 20},
|
||||
joinLabel: {color: '#1e1e2e', fontWeight: '600'},
|
||||
createButton: {backgroundColor: '#a6e3a1', borderColor: '#a6e3a1'},
|
||||
createLabel: {color: '#1e1e2e', fontWeight: '600'},
|
||||
bizTitle: {fontSize: 22, fontWeight: '700', color: '#cdd6f4', marginBottom: 6, textAlign: 'center'},
|
||||
bizSubtitle: {fontSize: 14, color: '#bac2de', textAlign: 'center', marginBottom: 20, paddingHorizontal: 20},
|
||||
joinError: {color: '#D92800', textAlign: 'center', marginTop: 8, marginBottom: 4, fontSize: 14, paddingHorizontal: 20},
|
||||
backButton: {backgroundColor: '#6c7086', borderColor: '#6c7086'},
|
||||
backLabel: {color: '#cdd6f4', fontWeight: '600'},
|
||||
warning: {
|
||||
color: '#dc2626',
|
||||
color: '#D92800',
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'center',
|
||||
paddingVertical: 8,
|
||||
@@ -578,7 +785,7 @@ const styles = StyleSheet.create({
|
||||
position: 'absolute',
|
||||
top: 10,
|
||||
left: 10,
|
||||
backgroundColor: 'rgba(0,0,0,0.6)',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.6)',
|
||||
borderRadius: 6,
|
||||
width: 36,
|
||||
height: 36,
|
||||
@@ -586,42 +793,64 @@ const styles = StyleSheet.create({
|
||||
justifyContent: 'center',
|
||||
zIndex: 10,
|
||||
},
|
||||
expandIcon: {color: '#ffffff', fontSize: 18, fontWeight: '700'},
|
||||
// Admin create button + scrollable cache list
|
||||
expandIcon: {color: '#cdd6f4', fontSize: 18, fontWeight: '700'},
|
||||
createCacheWrap: {
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 10,
|
||||
backgroundColor: '#ffffff',
|
||||
backgroundColor: '#313244',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#e5e7eb',
|
||||
borderBottomColor: '#45475a',
|
||||
},
|
||||
createCacheButton: {backgroundColor: '#2563eb', borderColor: '#2563eb', flex: 0},
|
||||
createCacheLabel: {color: '#ffffff', fontWeight: '600'},
|
||||
cacheListWrap: {flex: 1, backgroundColor: '#f3f4f6'},
|
||||
createCacheButton: {backgroundColor: '#bd93f9', borderColor: '#bd93f9', flex: 0},
|
||||
createCacheLabel: {color: '#1e1e2e', fontWeight: '600'},
|
||||
cacheListWrap: {flex: 1, backgroundColor: '#1e1e2e'},
|
||||
cacheListContent: {paddingHorizontal: 12, paddingTop: 10, paddingBottom: 12},
|
||||
// Cache editor form
|
||||
formSection: {flex: 1, paddingHorizontal: 12, paddingVertical: 10},
|
||||
formInput: {
|
||||
borderWidth: 1,
|
||||
borderColor: '#d1d5db',
|
||||
borderColor: '#45475a',
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 10,
|
||||
fontSize: 16,
|
||||
color: '#1f2937',
|
||||
backgroundColor: '#ffffff',
|
||||
color: '#cdd6f4',
|
||||
backgroundColor: '#313244',
|
||||
marginBottom: 10,
|
||||
},
|
||||
departmentLabel: {fontSize: 13, fontWeight: '600', color: '#bac2de', marginBottom: 6},
|
||||
departmentSelector: {
|
||||
borderWidth: 1,
|
||||
borderColor: '#45475a',
|
||||
borderRadius: 8,
|
||||
backgroundColor: '#313244',
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 10,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 8,
|
||||
},
|
||||
departmentSelectorText: {color: '#cdd6f4', fontSize: 15, fontWeight: '500'},
|
||||
departmentSelectorChevron: {color: '#6c7086', fontSize: 12, fontWeight: '700'},
|
||||
departmentMenu: {
|
||||
borderWidth: 1,
|
||||
borderColor: '#45475a',
|
||||
borderRadius: 8,
|
||||
backgroundColor: '#313244',
|
||||
marginBottom: 10,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
departmentOption: {paddingHorizontal: 12, paddingVertical: 10, borderBottomWidth: 1, borderBottomColor: '#45475a'},
|
||||
departmentOptionText: {color: '#cdd6f4', fontSize: 14},
|
||||
coordRow: {flexDirection: 'row', gap: 10},
|
||||
coordField: {flex: 1},
|
||||
coordLabel: {fontSize: 12, fontWeight: '600', color: '#6b7280', marginBottom: 4},
|
||||
saveButton: {backgroundColor: '#16a34a', borderColor: '#16a34a'},
|
||||
saveLabel: {color: '#ffffff', fontWeight: '600'},
|
||||
cancelButton: {backgroundColor: '#6b7280', borderColor: '#6b7280'},
|
||||
cancelLabel: {color: '#ffffff', fontWeight: '600'},
|
||||
// Player cache section
|
||||
cacheSection: {flex: 1, backgroundColor: '#f3f4f6', paddingHorizontal: 12, paddingTop: 10},
|
||||
emptyText: {color: '#9ca3af', textAlign: 'center', marginTop: 20, fontSize: 14},
|
||||
coordLabel: {fontSize: 12, fontWeight: '600', color: '#6c7086', marginBottom: 4},
|
||||
saveButton: {backgroundColor: '#a6e3a1', borderColor: '#a6e3a1'},
|
||||
saveLabel: {color: '#1e1e2e', fontWeight: '600'},
|
||||
cancelButton: {backgroundColor: '#6c7086', borderColor: '#6c7086'},
|
||||
cancelLabel: {color: '#cdd6f4', fontWeight: '600'},
|
||||
cacheSection: {flex: 1, backgroundColor: '#1e1e2e', paddingHorizontal: 12, paddingTop: 10},
|
||||
emptyText: {color: '#6c7086', textAlign: 'center', marginTop: 20, fontSize: 14},
|
||||
mapWrap: {flex: 1},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,32 +1,57 @@
|
||||
import React, {useCallback, useEffect, useState} from 'react';
|
||||
import {ActivityIndicator, Alert, ScrollView, StyleSheet, Text, View} from 'react-native';
|
||||
import {ActivityIndicator, Alert, Pressable, ScrollView, StyleSheet, Text, View} from 'react-native';
|
||||
import Screen from '../layout/Screen';
|
||||
import Card from '../UI/Card';
|
||||
import {Button} from '../UI/Button';
|
||||
import useGameHook from '../../hooks/useGameHook';
|
||||
import {getSession} from '../../hooks/SessionStore';
|
||||
|
||||
const PlayersScreen = () => {
|
||||
const PlayersScreen = ({navigation, route}) => {
|
||||
// Initialisation ------------
|
||||
|
||||
const session = getSession();
|
||||
const {getTeams, getTeamMembers, getGroupMembers, removeMember, resetPlayerProgress, getUser, getAdminWaitlist, approveAdmin, rejectAdmin} = useGameHook();
|
||||
const {getTeams, getTeamMembers, getGroupMembers, removeMember, resetPlayerProgress, getUser, getAdminWaitlist, approveAdmin, rejectAdmin, getSubgroups} = useGameHook();
|
||||
const isBusiness = Boolean(session.isBusiness);
|
||||
const isAdmin = Boolean(session.isAcceptedAdmin);
|
||||
const selectedDepartment = route?.params?.department || null;
|
||||
|
||||
// State ----------------------
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [players, setPlayers] = useState([]);
|
||||
const [departmentList, setDepartmentList] = useState([]);
|
||||
|
||||
// Handlers -------------------
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
if (!session.currentGid) { setLoading(false); return; }
|
||||
|
||||
const effectiveSGid = isBusiness
|
||||
? ((isAdmin && selectedDepartment?.SGid) ? selectedDepartment.SGid : (isAdmin ? null : session.currentSGid))
|
||||
: null;
|
||||
|
||||
if (isBusiness && isAdmin && !selectedDepartment) {
|
||||
const sgs = await getSubgroups(session.currentGid);
|
||||
setDepartmentList((sgs || []).filter((sg) => !sg.IsAdminGroup));
|
||||
setPlayers([]);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Load subgroup names for department display (business accounts)
|
||||
let subgroupNameMap = {};
|
||||
if (isBusiness) {
|
||||
const sgs = await getSubgroups(session.currentGid);
|
||||
for (const sg of (sgs || [])) {
|
||||
if (!sg.IsAdminGroup) subgroupNameMap[sg.SGid] = sg.SubGroupName;
|
||||
}
|
||||
}
|
||||
|
||||
// Get ALL members (including admins)
|
||||
const allMembers = await getGroupMembers(session.currentGid);
|
||||
const allMembers = await getGroupMembers(session.currentGid, effectiveSGid ?? null);
|
||||
|
||||
// Get admin waitlist entries
|
||||
const waitlist = await getAdminWaitlist(session.currentGid);
|
||||
const waitlist = effectiveSGid ? [] : await getAdminWaitlist(session.currentGid);
|
||||
|
||||
// Deduplicate allMembers by Uid — a user may have two rows if they were a
|
||||
// regular member before being approved as an admin. Prefer the admin row.
|
||||
@@ -36,7 +61,7 @@ const PlayersScreen = () => {
|
||||
.filter((m) => { if (seenUids.has(m.Uid)) return false; seenUids.add(m.Uid); return true; });
|
||||
|
||||
// Build team lookup: { Uid -> { teamCode, teamName, isLeader } }
|
||||
const allTeams = await getTeams(session.currentGid);
|
||||
const allTeams = await getTeams(session.currentGid, effectiveSGid ?? null);
|
||||
const teamInfoByUid = {};
|
||||
for (const team of (allTeams || [])) {
|
||||
const tms = await getTeamMembers(team.Tid);
|
||||
@@ -73,6 +98,7 @@ const PlayersScreen = () => {
|
||||
team: teamInfoByUid[m.Uid] || null,
|
||||
adminTag,
|
||||
waitlistId,
|
||||
departmentName: subgroupNameMap[m.SGid] || null,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -89,13 +115,14 @@ const PlayersScreen = () => {
|
||||
team: null,
|
||||
adminTag: '[Admin Awaiting Response]',
|
||||
waitlistId: w.id,
|
||||
departmentName: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setPlayers(playerList);
|
||||
setLoading(false);
|
||||
}, [session.currentGid]);
|
||||
}, [session.currentGid, session.currentUid, session.currentSGid, isBusiness, isAdmin, selectedDepartment, selectedDepartment?.SGid]);
|
||||
|
||||
useEffect(() => { loadData(); }, [loadData]);
|
||||
|
||||
@@ -152,8 +179,34 @@ const PlayersScreen = () => {
|
||||
);
|
||||
}
|
||||
|
||||
if (isBusiness && isAdmin && !selectedDepartment) {
|
||||
return (
|
||||
<Screen style={styles.container}>
|
||||
<Text style={styles.sectionTitle}>Departments</Text>
|
||||
<ScrollView style={styles.listSection}>
|
||||
{departmentList.map((department) => (
|
||||
<Pressable
|
||||
key={department.SGid}
|
||||
onPress={() => navigation.navigate('PlayersScreen', {department})}
|
||||
>
|
||||
<Card>
|
||||
<View style={styles.playerRow}>
|
||||
<Text style={styles.playerName}>{department.SubGroupName}</Text>
|
||||
<Text style={styles.openLabel}>Open</Text>
|
||||
</View>
|
||||
</Card>
|
||||
</Pressable>
|
||||
))}
|
||||
{departmentList.length === 0 && (
|
||||
<Text style={styles.emptyText}>No departments yet.</Text>
|
||||
)}
|
||||
</ScrollView>
|
||||
</Screen>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Screen style={styles.container}>
|
||||
<Screen style={styles.container} showBack={Boolean(selectedDepartment)}>
|
||||
<Text style={styles.sectionTitle}>Players</Text>
|
||||
<ScrollView style={styles.listSection}>
|
||||
{players.map((player) => (
|
||||
@@ -167,6 +220,12 @@ const PlayersScreen = () => {
|
||||
{' '}{player.adminTag}
|
||||
</Text>
|
||||
)}
|
||||
{/* Show department name in brackets for non-admin players (org accounts) */}
|
||||
{!player.adminTag && player.departmentName && (
|
||||
<Text style={styles.departmentName}>
|
||||
{' '}({player.departmentName})
|
||||
</Text>
|
||||
)}
|
||||
</Text>
|
||||
<View style={styles.playerMeta}>
|
||||
{player.team?.isLeader && (
|
||||
@@ -209,7 +268,7 @@ const PlayersScreen = () => {
|
||||
label="Kick"
|
||||
onClick={() => handleRemovePlayer(player.membershipId)}
|
||||
styleButton={styles.removeButton}
|
||||
styleLabel={styles.actionLabel}
|
||||
styleLabel={styles.kickLabel}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
@@ -228,38 +287,39 @@ const PlayersScreen = () => {
|
||||
const styles = StyleSheet.create({
|
||||
center: {justifyContent: 'center', alignItems: 'center', flex: 1},
|
||||
container: {padding: 0},
|
||||
body: {color: '#4b5563', fontSize: 15},
|
||||
body: {color: '#bac2de', fontSize: 15},
|
||||
sectionTitle: {
|
||||
fontSize: 20,
|
||||
fontWeight: '700',
|
||||
color: '#1f2937',
|
||||
color: '#cdd6f4',
|
||||
marginBottom: 12,
|
||||
paddingHorizontal: 15,
|
||||
paddingTop: 15,
|
||||
},
|
||||
openLabel: {fontSize: 13, fontWeight: '700', color: '#bd93f9'},
|
||||
listSection: {flex: 1, paddingHorizontal: 15},
|
||||
emptyText: {color: '#9ca3af', textAlign: 'center', marginTop: 30, fontSize: 14},
|
||||
emptyText: {color: '#6c7086', textAlign: 'center', marginTop: 30, fontSize: 14},
|
||||
playerRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
marginBottom: 8,
|
||||
},
|
||||
playerName: {fontSize: 15, fontWeight: '600', color: '#1f2937', flex: 1},
|
||||
playerName: {fontSize: 15, fontWeight: '600', color: '#cdd6f4', flex: 1},
|
||||
playerMeta: {flexDirection: 'row', alignItems: 'center', gap: 6},
|
||||
leaderBadge: {fontSize: 11, color: '#2563eb', fontWeight: '700'},
|
||||
teamCodeBadge: {fontSize: 12, fontWeight: '700', color: '#16a34a', letterSpacing: 1},
|
||||
adminBadge: {fontSize: 12, fontWeight: '700', color: '#dc2626'},
|
||||
waitlistBadge: {fontSize: 11, fontWeight: '600', color: '#f59e0b'},
|
||||
leaderBadge: {fontSize: 11, color: '#bd93f9', fontWeight: '700'},
|
||||
teamCodeBadge: {fontSize: 12, fontWeight: '700', color: '#a6e3a1', letterSpacing: 1},
|
||||
adminBadge: {fontSize: 12, fontWeight: '700', color: '#D92800'},
|
||||
waitlistBadge: {fontSize: 11, fontWeight: '600', color: '#f4a460'},
|
||||
actionRow: {flexDirection: 'row', gap: 8},
|
||||
resetButton: {backgroundColor: '#f59e0b', borderColor: '#f59e0b', minHeight: 36, flex: 1, paddingHorizontal: 10},
|
||||
removeButton: {backgroundColor: '#dc2626', borderColor: '#dc2626', minHeight: 36, flex: 1, paddingHorizontal: 10},
|
||||
actionLabel: {color: '#ffffff', fontWeight: '600', fontSize: 13},
|
||||
// Waitlisted admin card tint — light red so they read as pending, not regular players
|
||||
waitlistCard: {backgroundColor: '#FFD1DC', borderColor: '#fca5a5'},
|
||||
// Approve / deny buttons for waitlisted admins
|
||||
approveButton: {backgroundColor: '#16a34a', borderColor: '#16a34a', minHeight: 36, flex: 1, paddingHorizontal: 10},
|
||||
denyButton: {backgroundColor: '#dc2626', borderColor: '#dc2626', minHeight: 36, flex: 1, paddingHorizontal: 10},
|
||||
resetButton: {backgroundColor: '#86efac', borderColor: '#86efac', minHeight: 36, flex: 1, paddingHorizontal: 10},
|
||||
removeButton: {backgroundColor: '#D92800', borderColor: '#D92800', minHeight: 36, flex: 1, paddingHorizontal: 10},
|
||||
actionLabel: {color: '#1e1e2e', fontWeight: '600', fontSize: 13},
|
||||
kickLabel: {color: '#ffffff', fontWeight: '600', fontSize: 13},
|
||||
departmentName: {fontSize: 14, color: '#6c7086', fontStyle: 'italic'},
|
||||
waitlistCard: {backgroundColor: 'rgba(217, 40, 0, 0.15)', borderColor: '#D92800'},
|
||||
approveButton: {backgroundColor: '#a6e3a1', borderColor: '#a6e3a1', minHeight: 36, flex: 1, paddingHorizontal: 10},
|
||||
denyButton: {backgroundColor: '#D92800', borderColor: '#D92800', minHeight: 36, flex: 1, paddingHorizontal: 10},
|
||||
});
|
||||
|
||||
export default PlayersScreen;
|
||||
@@ -5,182 +5,175 @@ import Screen from '../layout/Screen';
|
||||
import {Button, ButtonTray} from '../UI/Button';
|
||||
|
||||
const RegisterScreen = ({navigation}) => {
|
||||
// Initialisation ------------
|
||||
const {
|
||||
username, email, password, confirmPassword, accountType,
|
||||
error, isLoading,
|
||||
setUsername, setEmail, setPassword, setConfirmPassword, setAccountType,
|
||||
register,
|
||||
} = useAuthHook();
|
||||
|
||||
const {
|
||||
username, email, password, confirmPassword, accountType,
|
||||
error, isLoading,
|
||||
setUsername, setEmail, setPassword, setConfirmPassword, setAccountType,
|
||||
register,
|
||||
} = useAuthHook();
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
// State ----------------------
|
||||
const handleRegister = async () => {
|
||||
setSubmitting(true);
|
||||
const user = await register();
|
||||
setSubmitting(false);
|
||||
if (!user) return;
|
||||
if (user.IsAcceptedAdmin) {
|
||||
navigation.reset({index: 0, routes: [{name: 'Admin'}]});
|
||||
return;
|
||||
}
|
||||
navigation.reset({index: 0, routes: [{name: 'Game'}]});
|
||||
};
|
||||
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
return (
|
||||
<Screen style={styles.container}>
|
||||
<View style={styles.content}>
|
||||
<Text style={styles.title}>Create Account</Text>
|
||||
|
||||
// Handlers -------------------
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Username"
|
||||
placeholderTextColor="#6c7086"
|
||||
value={username}
|
||||
onChangeText={setUsername}
|
||||
editable={!isLoading}
|
||||
/>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Email"
|
||||
placeholderTextColor="#6c7086"
|
||||
autoCapitalize="none"
|
||||
keyboardType="email-address"
|
||||
value={email}
|
||||
onChangeText={setEmail}
|
||||
editable={!isLoading}
|
||||
/>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Password"
|
||||
placeholderTextColor="#6c7086"
|
||||
secureTextEntry
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
editable={!isLoading}
|
||||
/>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Confirm Password"
|
||||
placeholderTextColor="#6c7086"
|
||||
secureTextEntry
|
||||
value={confirmPassword}
|
||||
onChangeText={setConfirmPassword}
|
||||
editable={!isLoading}
|
||||
/>
|
||||
|
||||
const handleRegister = async () => {
|
||||
setSubmitting(true);
|
||||
const user = await register();
|
||||
setSubmitting(false);
|
||||
if (!user) return;
|
||||
if (user.IsAcceptedAdmin) {
|
||||
navigation.reset({index: 0, routes: [{name: 'Admin'}]});
|
||||
return;
|
||||
}
|
||||
navigation.reset({index: 0, routes: [{name: 'Game'}]});
|
||||
};
|
||||
<View style={styles.typeRow}>
|
||||
<Pressable
|
||||
style={[styles.typeButton, accountType === 'Individual' && styles.typeButtonActive]}
|
||||
onPress={() => setAccountType('Individual')}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<Text style={[styles.typeText, accountType === 'Individual' && styles.typeTextActive]}>
|
||||
Individual
|
||||
</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
style={[styles.typeButton, accountType === 'Business/School' && styles.typeButtonActive]}
|
||||
onPress={() => setAccountType('Business/School')}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<Text style={[styles.typeText, accountType === 'Business/School' && styles.typeTextActive]}>
|
||||
Business
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
// View -----------------------
|
||||
{error ? <Text style={styles.errorText}>{error}</Text> : null}
|
||||
|
||||
return (
|
||||
<Screen style={styles.container}>
|
||||
<View style={styles.content}>
|
||||
<Text style={styles.title}>Create Account</Text>
|
||||
<ButtonTray>
|
||||
<Button
|
||||
label={submitting ? 'Creating...' : 'Create Account'}
|
||||
onClick={handleRegister}
|
||||
styleButton={styles.submitButton}
|
||||
styleLabel={styles.submitText}
|
||||
/>
|
||||
</ButtonTray>
|
||||
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Username"
|
||||
placeholderTextColor="#9ca3af"
|
||||
value={username}
|
||||
onChangeText={setUsername}
|
||||
editable={!isLoading}
|
||||
/>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Email"
|
||||
placeholderTextColor="#9ca3af"
|
||||
autoCapitalize="none"
|
||||
keyboardType="email-address"
|
||||
value={email}
|
||||
onChangeText={setEmail}
|
||||
editable={!isLoading}
|
||||
/>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Password"
|
||||
placeholderTextColor="#9ca3af"
|
||||
secureTextEntry
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
editable={!isLoading}
|
||||
/>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Confirm Password"
|
||||
placeholderTextColor="#9ca3af"
|
||||
secureTextEntry
|
||||
value={confirmPassword}
|
||||
onChangeText={setConfirmPassword}
|
||||
editable={!isLoading}
|
||||
/>
|
||||
|
||||
<View style={styles.typeRow}>
|
||||
<Pressable
|
||||
style={[styles.typeButton, accountType === 'Individual' && styles.typeButtonActive]}
|
||||
onPress={() => setAccountType('Individual')}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<Text style={[styles.typeText, accountType === 'Individual' && styles.typeTextActive]}>
|
||||
Individual
|
||||
</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
style={[styles.typeButton, accountType === 'Business/School' && styles.typeButtonActive]}
|
||||
onPress={() => setAccountType('Business/School')}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<Text style={[styles.typeText, accountType === 'Business/School' && styles.typeTextActive]}>
|
||||
Business
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
{error ? <Text style={styles.errorText}>{error}</Text> : null}
|
||||
|
||||
<ButtonTray>
|
||||
<Button
|
||||
label={submitting ? 'Creating...' : 'Create Account'}
|
||||
onClick={handleRegister}
|
||||
styleButton={styles.submitButton}
|
||||
styleLabel={styles.submitText}
|
||||
/>
|
||||
</ButtonTray>
|
||||
|
||||
<Pressable onPress={() => navigation.navigate('Auth', {screen: 'LoginScreen'})}>
|
||||
<Text style={styles.linkText}>Back to Login</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</Screen>
|
||||
);
|
||||
<Pressable onPress={() => navigation.navigate('Auth', {screen: 'LoginScreen'})}>
|
||||
<Text style={styles.linkText}>Back to Login</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</Screen>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
justifyContent: 'center',
|
||||
},
|
||||
content: {
|
||||
gap: 12,
|
||||
},
|
||||
title: {
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'center',
|
||||
marginBottom: 10,
|
||||
},
|
||||
input: {
|
||||
borderWidth: 1,
|
||||
borderColor: '#d1d5db',
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 10,
|
||||
fontSize: 16,
|
||||
color: '#1f2937',
|
||||
backgroundColor: '#ffffff',
|
||||
},
|
||||
typeRow: {
|
||||
flexDirection: 'row',
|
||||
gap: 10,
|
||||
},
|
||||
typeButton: {
|
||||
flex: 1,
|
||||
borderWidth: 1,
|
||||
borderColor: '#cbd5e1',
|
||||
borderRadius: 8,
|
||||
alignItems: 'center',
|
||||
paddingVertical: 10,
|
||||
},
|
||||
typeButtonActive: {
|
||||
backgroundColor: '#2563eb',
|
||||
borderColor: '#2563eb',
|
||||
},
|
||||
typeText: {
|
||||
color: '#1f2937',
|
||||
fontWeight: '600',
|
||||
},
|
||||
typeTextActive: {
|
||||
color: '#ffffff',
|
||||
},
|
||||
errorText: {
|
||||
color: '#dc2626',
|
||||
textAlign: 'center',
|
||||
marginTop: 5,
|
||||
},
|
||||
submitButton: {
|
||||
backgroundColor: '#2563eb',
|
||||
borderColor: '#2563eb',
|
||||
},
|
||||
submitText: {
|
||||
color: '#ffffff',
|
||||
fontWeight: '600',
|
||||
},
|
||||
linkText: {
|
||||
textAlign: 'center',
|
||||
color: '#2563eb',
|
||||
fontWeight: '600',
|
||||
paddingVertical: 10,
|
||||
},
|
||||
container: {
|
||||
justifyContent: 'center',
|
||||
},
|
||||
content: {
|
||||
gap: 12,
|
||||
},
|
||||
title: {
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'center',
|
||||
marginBottom: 10,
|
||||
color: '#cdd6f4',
|
||||
},
|
||||
input: {
|
||||
borderWidth: 1,
|
||||
borderColor: '#45475a',
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 10,
|
||||
fontSize: 16,
|
||||
color: '#cdd6f4',
|
||||
backgroundColor: '#313244',
|
||||
},
|
||||
typeRow: {
|
||||
flexDirection: 'row',
|
||||
gap: 10,
|
||||
},
|
||||
typeButton: {
|
||||
flex: 1,
|
||||
borderWidth: 1,
|
||||
borderColor: '#45475a',
|
||||
borderRadius: 8,
|
||||
alignItems: 'center',
|
||||
paddingVertical: 10,
|
||||
},
|
||||
typeButtonActive: {
|
||||
backgroundColor: '#bd93f9',
|
||||
borderColor: '#bd93f9',
|
||||
},
|
||||
typeText: {
|
||||
color: '#bac2de',
|
||||
fontWeight: '600',
|
||||
},
|
||||
typeTextActive: {
|
||||
color: '#1e1e2e',
|
||||
},
|
||||
errorText: {
|
||||
color: '#D92800',
|
||||
textAlign: 'center',
|
||||
marginTop: 5,
|
||||
},
|
||||
submitButton: {
|
||||
backgroundColor: '#bd93f9',
|
||||
borderColor: '#bd93f9',
|
||||
},
|
||||
submitText: {
|
||||
color: '#1e1e2e',
|
||||
fontWeight: '600',
|
||||
},
|
||||
linkText: {
|
||||
textAlign: 'center',
|
||||
color: '#bd93f9',
|
||||
fontWeight: '600',
|
||||
paddingVertical: 10,
|
||||
},
|
||||
});
|
||||
|
||||
export default RegisterScreen;
|
||||
@@ -7,43 +7,55 @@ import useGameHook from '../../hooks/useGameHook';
|
||||
import {getSession, setSessionTeam} from '../../hooks/SessionStore';
|
||||
|
||||
const TeamScreen = () => {
|
||||
// Initialisation ------------
|
||||
const session = getSession();
|
||||
const isAdmin = session.isAcceptedAdmin;
|
||||
const isBusiness = Boolean(session.isBusiness);
|
||||
const {getLobby, getSubgroup, getTeam, createTeam, joinTeamByCode, getTeamMembers, leaveTeam, getUser, updateUser, updateTeam, getAdminWaitlist} = useGameHook();
|
||||
|
||||
const session = getSession();
|
||||
const isAdmin = session.isAcceptedAdmin;
|
||||
const {getTeam, createTeam, joinTeamByCode, getTeamMembers, leaveTeam, getUser, updateUser, updateTeam, getAdminWaitlist} = useGameHook();
|
||||
const [activeTid, setActiveTid] = useState(session.currentTid);
|
||||
const [teamCode, setTeamCode] = useState('');
|
||||
const [team, setTeam] = useState(null);
|
||||
const [members, setMembers] = useState([]);
|
||||
const [memberNames, setMemberNames] = useState({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [teamsDisabled, setTeamsDisabled] = useState(false);
|
||||
const [onAdminWaitlist, setOnAdminWaitlist] = useState(false);
|
||||
const [editingName, setEditingName] = useState(false);
|
||||
const [teamNameDraft, setTeamNameDraft] = useState('');
|
||||
|
||||
// State ----------------------
|
||||
|
||||
const [activeTid, setActiveTid] = useState(session.currentTid);
|
||||
const [teamCode, setTeamCode] = useState('');
|
||||
const [team, setTeam] = useState(null);
|
||||
const [members, setMembers] = useState([]);
|
||||
const [memberNames, setMemberNames] = useState({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [onAdminWaitlist, setOnAdminWaitlist] = useState(false);
|
||||
const [editingName, setEditingName] = useState(false);
|
||||
const [teamNameDraft, setTeamNameDraft] = useState('');
|
||||
|
||||
// Handlers -------------------
|
||||
|
||||
const loadTeamData = async (tid) => {
|
||||
const t = await getTeam(tid);
|
||||
setTeam(t);
|
||||
const ms = await getTeamMembers(tid);
|
||||
setMembers(ms || []);
|
||||
const names = {};
|
||||
for (const m of (ms || [])) {
|
||||
const u = await getUser(m.Uid);
|
||||
if (u) names[m.Uid] = u.username;
|
||||
}
|
||||
setMemberNames(names);
|
||||
};
|
||||
const loadTeamData = async (tid) => {
|
||||
const t = await getTeam(tid);
|
||||
setTeam(t);
|
||||
const ms = await getTeamMembers(tid);
|
||||
setMembers(ms || []);
|
||||
const names = {};
|
||||
for (const m of (ms || [])) {
|
||||
const u = await getUser(m.Uid);
|
||||
if (u) names[m.Uid] = u.username;
|
||||
}
|
||||
setMemberNames(names);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
// Check if the user is on the admin waitlist
|
||||
if (session.currentGid && !activeTid) {
|
||||
if (!session.currentGid) { setLoading(false); return; }
|
||||
|
||||
const group = await getLobby(session.currentGid);
|
||||
let teamsAreEnabled = Boolean(group?.TeamsEnabled);
|
||||
|
||||
if (isBusiness && session.currentSGid) {
|
||||
const subgroup = await getSubgroup(session.currentSGid);
|
||||
if (subgroup) teamsAreEnabled = Boolean(subgroup.TeamsEnabled);
|
||||
}
|
||||
|
||||
if (!teamsAreEnabled) {
|
||||
setTeamsDisabled(true);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
setTeamsDisabled(false);
|
||||
|
||||
if (!activeTid) {
|
||||
const waitlist = await getAdminWaitlist(session.currentGid, session.currentUid);
|
||||
if (waitlist.length > 0) {
|
||||
setOnAdminWaitlist(true);
|
||||
@@ -61,7 +73,11 @@ const TeamScreen = () => {
|
||||
if (!teamCode.trim()) return;
|
||||
// Force uppercase just in case a lowercase code is pasted
|
||||
const code = teamCode.trim().toUpperCase();
|
||||
const result = await joinTeamByCode({JoinCode: code, Uid: session.currentUid});
|
||||
const result = await joinTeamByCode({
|
||||
JoinCode: code,
|
||||
Uid: session.currentUid,
|
||||
ExpectedSGid: session.currentSGid,
|
||||
});
|
||||
if (!result) return;
|
||||
|
||||
// If the server indicates this code matched the admin join code
|
||||
@@ -126,6 +142,17 @@ const TeamScreen = () => {
|
||||
);
|
||||
}
|
||||
|
||||
if (teamsDisabled) {
|
||||
return (
|
||||
<Screen style={styles.center}>
|
||||
<View style={styles.disabledWrap}>
|
||||
<Text style={styles.disabledTitle}>Teams Disabled</Text>
|
||||
<Text style={styles.disabledBody}>The admin has not enabled teams for this game.</Text>
|
||||
</View>
|
||||
</Screen>
|
||||
);
|
||||
}
|
||||
|
||||
// Waiting for admin approval
|
||||
if (onAdminWaitlist) {
|
||||
return (
|
||||
@@ -270,64 +297,65 @@ const TeamScreen = () => {
|
||||
const styles = StyleSheet.create({
|
||||
center: {justifyContent: 'center', alignItems: 'center'},
|
||||
container: {padding: 0},
|
||||
// Shown when the user has not yet joined a game
|
||||
notInGameText: {color: '#6b7280', fontSize: 15, fontWeight: '600', marginBottom: 16, textAlign: 'center', paddingHorizontal: 20},
|
||||
inputDisabled: {backgroundColor: '#f3f4f6', opacity: 0.5},
|
||||
disabledWrap: {paddingHorizontal: 30, alignItems: 'center', opacity: 0.45},
|
||||
disabledTitle: {fontSize: 20, fontWeight: '700', color: '#ffffff', marginBottom: 10, textAlign: 'center'},
|
||||
disabledBody: {fontSize: 15, color: '#ffffff', textAlign: 'center'},
|
||||
notInGameText: {color: '#ffffff', fontSize: 15, fontWeight: '600', marginBottom: 16, textAlign: 'center', paddingHorizontal: 20},
|
||||
inputDisabled: {backgroundColor: '#313244', opacity: 0.5},
|
||||
inputRow: {flexDirection: 'row', gap: 10, marginBottom: 15, width: '100%', paddingHorizontal: 20},
|
||||
fullRow: {width: '100%', paddingHorizontal: 20},
|
||||
codeInput: {
|
||||
flex: 1,
|
||||
borderWidth: 1,
|
||||
borderColor: '#d1d5db',
|
||||
borderColor: '#45475a',
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 10,
|
||||
fontSize: 16,
|
||||
color: '#1f2937',
|
||||
backgroundColor: '#ffffff',
|
||||
color: '#ffffff',
|
||||
backgroundColor: '#313244',
|
||||
},
|
||||
joinButton: {backgroundColor: '#2563eb', borderColor: '#2563eb', flex: 0, paddingHorizontal: 20},
|
||||
joinLabel: {color: '#ffffff', fontWeight: '600'},
|
||||
createButton: {backgroundColor: '#16a34a', borderColor: '#16a34a'},
|
||||
createLabel: {color: '#ffffff', fontWeight: '600'},
|
||||
joinButton: {backgroundColor: '#bd93f9', borderColor: '#bd93f9', flex: 0, paddingHorizontal: 20},
|
||||
joinLabel: {color: '#1e1e2e', fontWeight: '600'},
|
||||
createButton: {backgroundColor: '#a6e3a1', borderColor: '#a6e3a1'},
|
||||
createLabel: {color: '#1e1e2e', fontWeight: '600'},
|
||||
waitlistText: {
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
color: '#374151',
|
||||
color: '#ffffff',
|
||||
textAlign: 'center',
|
||||
paddingHorizontal: 30,
|
||||
},
|
||||
teamHeader: {
|
||||
backgroundColor: '#374151',
|
||||
backgroundColor: '#45475a',
|
||||
paddingVertical: 14,
|
||||
paddingHorizontal: 16,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
},
|
||||
teamCodeLabel: {color: '#d1d5db', fontSize: 15, fontWeight: '600'},
|
||||
teamCodeLabel: {color: '#ffffff', fontSize: 15, fontWeight: '600'},
|
||||
teamCodeValue: {color: '#ffffff', fontSize: 17, fontWeight: '700', letterSpacing: 2},
|
||||
// Team name / rename section
|
||||
teamNameSection: {paddingHorizontal: 15, paddingVertical: 10, borderBottomWidth: 1, borderBottomColor: '#e5e7eb'},
|
||||
teamNameText: {fontSize: 18, fontWeight: '700', color: '#1f2937', flex: 1},
|
||||
teamNameSection: {paddingHorizontal: 15, paddingVertical: 10, borderBottomWidth: 1, borderBottomColor: '#45475a'},
|
||||
teamNameText: {fontSize: 18, fontWeight: '700', color: '#ffffff', flex: 1},
|
||||
renameRow: {flexDirection: 'row', alignItems: 'center', gap: 8},
|
||||
renameInput: {
|
||||
flex: 1,
|
||||
borderWidth: 1,
|
||||
borderColor: '#d1d5db',
|
||||
borderColor: '#45475a',
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 8,
|
||||
fontSize: 16,
|
||||
color: '#1f2937',
|
||||
backgroundColor: '#ffffff',
|
||||
color: '#ffffff',
|
||||
backgroundColor: '#313244',
|
||||
},
|
||||
renameButton: {backgroundColor: '#2563eb', borderColor: '#2563eb', flex: 0, minHeight: 36, paddingHorizontal: 14},
|
||||
renameLabel: {color: '#ffffff', fontWeight: '600', fontSize: 13},
|
||||
renameSaveButton: {backgroundColor: '#16a34a', borderColor: '#16a34a', flex: 0, minHeight: 36, paddingHorizontal: 12},
|
||||
renameSaveLabel: {color: '#ffffff', fontWeight: '600', fontSize: 13},
|
||||
renameCancelButton: {backgroundColor: '#6b7280', borderColor: '#6b7280', flex: 0, minHeight: 36, paddingHorizontal: 12},
|
||||
renameCancelLabel: {color: '#ffffff', fontWeight: '600', fontSize: 13},
|
||||
renameButton: {backgroundColor: '#bd93f9', borderColor: '#bd93f9', flex: 0, minHeight: 36, paddingHorizontal: 14},
|
||||
renameLabel: {color: '#1e1e2e', fontWeight: '600', fontSize: 13},
|
||||
renameSaveButton: {backgroundColor: '#a6e3a1', borderColor: '#a6e3a1', flex: 0, minHeight: 36, paddingHorizontal: 12},
|
||||
renameSaveLabel: {color: '#1e1e2e', fontWeight: '600', fontSize: 13},
|
||||
renameCancelButton: {backgroundColor: '#6c7086', borderColor: '#6c7086', flex: 0, minHeight: 36, paddingHorizontal: 12},
|
||||
renameCancelLabel: {color: '#cdd6f4', fontWeight: '600', fontSize: 13},
|
||||
memberList: {flex: 1, paddingHorizontal: 15, paddingTop: 10},
|
||||
memberRow: {
|
||||
flexDirection: 'row',
|
||||
@@ -335,16 +363,15 @@ const styles = StyleSheet.create({
|
||||
alignItems: 'center',
|
||||
},
|
||||
memberInfo: {flex: 1, flexDirection: 'row', alignItems: 'center', gap: 6},
|
||||
memberName: {fontSize: 16, color: '#1f2937', fontWeight: '600'},
|
||||
leaderBadge: {fontSize: 12, color: '#2563eb', fontWeight: '700'},
|
||||
kickButton: {backgroundColor: '#dc2626', borderColor: '#dc2626', minHeight: 36, flex: 0, paddingHorizontal: 14},
|
||||
kickLabel: {color: '#ffffff', fontWeight: '600', fontSize: 13},
|
||||
emptyText: {color: '#9ca3af', textAlign: 'center', marginTop: 30, fontSize: 14},
|
||||
memberName: {fontSize: 16, color: '#ffffff', fontWeight: '600'},
|
||||
leaderBadge: {fontSize: 12, color: '#bd93f9', fontWeight: '700'},
|
||||
kickButton: {backgroundColor: '#D92800', borderColor: '#D92800', minHeight: 36, flex: 0, paddingHorizontal: 14},
|
||||
kickLabel: {color: '#1e1e2e', fontWeight: '600', fontSize: 13},
|
||||
emptyText: {color: '#ffffff', textAlign: 'center', marginTop: 30, fontSize: 14},
|
||||
leaveWrap: {paddingHorizontal: 0, paddingTop: 10, paddingBottom: 16},
|
||||
leaveButton: {backgroundColor: '#dc2626', borderColor: '#dc2626'},
|
||||
leaveLabel: {color: '#ffffff', fontWeight: '600'},
|
||||
// Current user self-highlight (light green card)
|
||||
selfCard: {backgroundColor: '#dcfce7', borderColor: '#86efac'},
|
||||
leaveButton: {backgroundColor: '#D92800', borderColor: '#D92800'},
|
||||
leaveLabel: {color: '#1e1e2e', fontWeight: '600'},
|
||||
selfCard: {backgroundColor: 'rgba(166, 227, 161, 0.15)', borderColor: '#a6e3a1'},
|
||||
});
|
||||
|
||||
export default TeamScreen;
|
||||
@@ -41,6 +41,15 @@ const useGameHook = () => {
|
||||
return response.isSuccess ? response.result : null;
|
||||
};
|
||||
|
||||
// Look up a group by its Organisation Join Code
|
||||
const getGroupByOrgCode = async (code) => {
|
||||
const normalizedCode = String(code || '').trim().toUpperCase();
|
||||
if (!normalizedCode) return null;
|
||||
const response = await API.get(`${groupsEndpoint}?OrgJoinCode=${encodeURIComponent(normalizedCode)}`);
|
||||
if (response.isSuccess && response.result.length > 0) return response.result[0];
|
||||
return null;
|
||||
};
|
||||
|
||||
// Subgroups
|
||||
const getSubgroups = async (gid) => {
|
||||
const response = await API.get(`${subgroupsEndpoint}?Gid=${gid}`);
|
||||
@@ -69,12 +78,21 @@ const useGameHook = () => {
|
||||
|
||||
// Subgroup members
|
||||
const joinPrivateGame = async (payload) => {
|
||||
const response = await API.post(subgroupMembersEndpoint, payload);
|
||||
const data = {
|
||||
JoinCode: payload.JoinCode,
|
||||
Uid: payload.Uid,
|
||||
};
|
||||
if (payload.ExpectedGid !== undefined && payload.ExpectedGid !== null) {
|
||||
data.ExpectedGid = payload.ExpectedGid;
|
||||
}
|
||||
const response = await API.post(subgroupMembersEndpoint, data);
|
||||
return response.isSuccess ? response.result : null;
|
||||
};
|
||||
|
||||
const getGroupMembers = async (gid) => {
|
||||
const response = await API.get(`${subgroupMembersEndpoint}?Gid=${gid}`);
|
||||
const getGroupMembers = async (gid, sgid = null) => {
|
||||
let url = `${subgroupMembersEndpoint}?Gid=${gid}`;
|
||||
if (sgid !== null && sgid !== undefined) url += `&SGid=${sgid}`;
|
||||
const response = await API.get(url);
|
||||
return response.isSuccess ? response.result : [];
|
||||
};
|
||||
|
||||
@@ -84,8 +102,10 @@ const useGameHook = () => {
|
||||
};
|
||||
|
||||
// Teams
|
||||
const getTeams = async (gid) => {
|
||||
const response = await API.get(`${teamsEndpoint}?Gid=${gid}`);
|
||||
const getTeams = async (gid, sgid = null) => {
|
||||
let url = `${teamsEndpoint}?Gid=${gid}`;
|
||||
if (sgid !== null && sgid !== undefined) url += `&SGid=${sgid}`;
|
||||
const response = await API.get(url);
|
||||
return response.isSuccess ? response.result : [];
|
||||
};
|
||||
|
||||
@@ -109,8 +129,21 @@ const useGameHook = () => {
|
||||
return response.isSuccess;
|
||||
};
|
||||
|
||||
const disbandTeams = async (gid) => {
|
||||
const response = await API.post(`${teamsEndpoint}/disband?Gid=${gid}`, {});
|
||||
return response.isSuccess;
|
||||
};
|
||||
|
||||
const joinTeamByCode = async (payload) => {
|
||||
const response = await API.post(teamMembersEndpoint, payload);
|
||||
const data = {
|
||||
Uid: payload.Uid,
|
||||
};
|
||||
if (payload.JoinCode !== undefined) data.JoinCode = payload.JoinCode;
|
||||
if (payload.Tid !== undefined) data.Tid = payload.Tid;
|
||||
if (payload.ExpectedSGid !== undefined && payload.ExpectedSGid !== null) {
|
||||
data.ExpectedSGid = payload.ExpectedSGid;
|
||||
}
|
||||
const response = await API.post(teamMembersEndpoint, data);
|
||||
return response.isSuccess ? response.result : null;
|
||||
};
|
||||
|
||||
@@ -163,9 +196,11 @@ const useGameHook = () => {
|
||||
return response.isSuccess;
|
||||
};
|
||||
|
||||
// Game reset — remove all caches for a game
|
||||
const resetGame = async (gid) => {
|
||||
const response = await API.post(`${cachesEndpoint}/reset?Gid=${gid}`, {});
|
||||
// Game reset — remove all caches for a game (optionally scoped to a subgroup)
|
||||
const resetGame = async (gid, sgid = null) => {
|
||||
let url = `${cachesEndpoint}/reset?Gid=${gid}`;
|
||||
if (sgid !== null) url += `&SGid=${sgid}`;
|
||||
const response = await API.post(url, {});
|
||||
return response.isSuccess;
|
||||
};
|
||||
|
||||
@@ -175,6 +210,11 @@ const useGameHook = () => {
|
||||
return response.isSuccess;
|
||||
};
|
||||
|
||||
const resetTeamProgress = async (gid, tid) => {
|
||||
const response = await API.post(`${cachesEndpoint}/reset-team/${tid}?Gid=${gid}`, {});
|
||||
return response.isSuccess;
|
||||
};
|
||||
|
||||
// Admin waitlist
|
||||
const getAdminWaitlist = async (gid, uid = null) => {
|
||||
let url = `${adminWaitlistEndpoint}?Gid=${gid}`;
|
||||
@@ -201,6 +241,7 @@ const useGameHook = () => {
|
||||
createPrivateGame,
|
||||
getLobby,
|
||||
updateGroup,
|
||||
getGroupByOrgCode,
|
||||
getSubgroups,
|
||||
getSubgroup,
|
||||
createSubgroup,
|
||||
@@ -214,6 +255,7 @@ const useGameHook = () => {
|
||||
createTeam,
|
||||
updateTeam,
|
||||
deleteTeam,
|
||||
disbandTeams,
|
||||
joinTeamByCode,
|
||||
getTeamMembers,
|
||||
leaveTeam,
|
||||
@@ -223,6 +265,7 @@ const useGameHook = () => {
|
||||
deleteCache,
|
||||
resetGame,
|
||||
resetPlayerProgress,
|
||||
resetTeamProgress,
|
||||
getAdminWaitlist,
|
||||
approveAdmin,
|
||||
rejectAdmin,
|
||||
|
||||
Reference in new issue
Block a user