Merge pull request #6 from mudabbir-ahmad/Business-stuff

Business-stuff
This commit is contained in:
bobbert authored and GitHub committed 2026-04-09 14:28:48 +01:00
commit 021dbaa7c7
20 files changed
+1211 -434

No files matched your search

+2
View File
@@ -9,6 +9,7 @@ import TeamScreen from "./src/components/screens/TeamScreen";
import MapScreen from "./src/components/screens/MapScreen"; import MapScreen from "./src/components/screens/MapScreen";
import LeaderboardScreen from "./src/components/screens/LeaderboardScreen"; import LeaderboardScreen from "./src/components/screens/LeaderboardScreen";
import GameSettingsScreen from "./src/components/screens/GameSettingsScreen"; import GameSettingsScreen from "./src/components/screens/GameSettingsScreen";
import DepartmentSettingsScreen from "./src/components/screens/DepartmentSettingsScreen";
import PlayersScreen from "./src/components/screens/PlayersScreen"; import PlayersScreen from "./src/components/screens/PlayersScreen";
import ExpandedMapScreen from "./src/components/screens/ExpandedMapScreen"; import ExpandedMapScreen from "./src/components/screens/ExpandedMapScreen";
import {loadSession} from "./src/hooks/SessionStore"; import {loadSession} from "./src/hooks/SessionStore";
@@ -43,6 +44,7 @@ const GameStack = () => (
const AdminStack = () => ( const AdminStack = () => (
<AdminStackNav.Navigator screenOptions={screenOptions}> <AdminStackNav.Navigator screenOptions={screenOptions}>
<AdminStackNav.Screen name="GameSettingsScreen" component={GameSettingsScreen}/> <AdminStackNav.Screen name="GameSettingsScreen" component={GameSettingsScreen}/>
<AdminStackNav.Screen name="DepartmentSettingsScreen" component={DepartmentSettingsScreen}/>
<AdminStackNav.Screen name="MapScreen" component={MapScreen}/> <AdminStackNav.Screen name="MapScreen" component={MapScreen}/>
<AdminStackNav.Screen name="LeaderboardScreen" component={LeaderboardScreen}/> <AdminStackNav.Screen name="LeaderboardScreen" component={LeaderboardScreen}/>
<AdminStackNav.Screen name="PlayersScreen" component={PlayersScreen}/> <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

+2 -2
View File
@@ -13,12 +13,12 @@ const Card = ({children, style}) => {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
card: { card: {
backgroundColor: '#ffffff', backgroundColor: '#313244',
borderRadius: 8, borderRadius: 8,
padding: 12, padding: 12,
marginBottom: 8, marginBottom: 8,
borderWidth: 1, borderWidth: 1,
borderColor: '#e5e7eb', borderColor: '#45475a',
}, },
}); });
+25 -27
View File
@@ -2,27 +2,24 @@ import {StyleSheet, Text, View} from 'react-native';
import Card from '../UI/Card'; import Card from '../UI/Card';
import {Button} from '../UI/Button'; import {Button} from '../UI/Button';
const CacheCardItem = ({cache, isAdmin, isClaimed, isSelected, onEdit, onDelete, onSelect, disabled}) => { const CacheCardItem = ({cache, isAdmin, isClaimed, isSelected, onEdit, onDelete, onSelect, disabled, departmentName}) => {
// Initialisation ------------- // 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 ( return (
<Card style={[isSelected && styles.selectedCard, isClaimed && styles.claimedCard, disabled && styles.disabledCard]}> <Card style={[isSelected && styles.selectedCard, isClaimed && styles.claimedCard, disabled && styles.disabledCard]}>
<View style={styles.topRow}> <View style={styles.topRow}>
<View style={styles.nameWrap}> <View style={styles.nameWrap}>
<Text style={[styles.name, isClaimed && styles.claimedText, disabled && styles.disabledText]} numberOfLines={1}>{displayName}</Text> <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 ? ( {cache.name ? (
<Text style={styles.clue} numberOfLines={1}>{cache.clue}</Text> <Text style={styles.clue} numberOfLines={1}>{cache.clue}</Text>
) : null} ) : null}
</View> </View>
{/* Players see team-specific claim status; admins see no status */}
{!isAdmin && !disabled && ( {!isAdmin && !disabled && (
<Text style={[styles.status, {color: isClaimed ? '#9ca3af' : '#16a34a'}]}> <Text style={[styles.status, {color: isClaimed ? '#585b70' : '#a6e3a1'}]}>
{isClaimed ? 'Claimed' : 'Available'} {isClaimed ? 'Claimed' : 'Available'}
</Text> </Text>
)} )}
@@ -86,54 +83,55 @@ const styles = StyleSheet.create({
alignItems: 'center', alignItems: 'center',
marginBottom: 8, 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}, nameWrap: {flex: 1, marginRight: 8},
name: {fontSize: 15, fontWeight: '600', color: '#1f2937'}, name: {fontSize: 15, fontWeight: '600', color: '#cdd6f4'},
claimedText: {color: '#9ca3af'}, claimedText: {color: '#6c7086'},
status: {fontSize: 13, fontWeight: '600'}, status: {fontSize: 13, fontWeight: '600'},
actions: {flexDirection: 'row', gap: 8}, actions: {flexDirection: 'row', gap: 8},
editBtn: { editBtn: {
backgroundColor: '#2563eb', backgroundColor: '#bd93f9',
borderColor: '#2563eb', borderColor: '#bd93f9',
minHeight: 36, minHeight: 36,
paddingHorizontal: 14, paddingHorizontal: 14,
flex: 1, flex: 1,
}, },
deleteBtn: { deleteBtn: {
backgroundColor: '#dc2626', backgroundColor: '#D92800',
borderColor: '#dc2626', borderColor: '#D92800',
minHeight: 36, minHeight: 36,
paddingHorizontal: 14, paddingHorizontal: 14,
flex: 1, flex: 1,
}, },
selectBtn: { selectBtn: {
backgroundColor: '#16a34a', backgroundColor: '#a6e3a1',
borderColor: '#16a34a', borderColor: '#a6e3a1',
minHeight: 36, minHeight: 36,
paddingHorizontal: 14, paddingHorizontal: 14,
flex: 1, flex: 1,
}, },
selectedBtn: { selectedBtn: {
backgroundColor: '#6b7280', backgroundColor: '#6c7086',
borderColor: '#6b7280', borderColor: '#6c7086',
minHeight: 36, minHeight: 36,
paddingHorizontal: 14, paddingHorizontal: 14,
flex: 1, flex: 1,
}, },
claimedBtn: { claimedBtn: {
backgroundColor: '#d1d5db', backgroundColor: '#45475a',
borderColor: '#d1d5db', borderColor: '#45475a',
minHeight: 36, minHeight: 36,
paddingHorizontal: 14, paddingHorizontal: 14,
flex: 1, flex: 1,
}, },
claimedBtnLabel: {color: '#6b7280', fontWeight: '600', fontSize: 13}, claimedBtnLabel: {color: '#6c7086', fontWeight: '600', fontSize: 13},
selectedBtnLabel: {color: '#ffffff', fontWeight: '600', fontSize: 13}, selectedBtnLabel: {color: '#cdd6f4', fontWeight: '600', fontSize: 13},
selectedCard: {borderColor: '#2563eb', borderWidth: 2}, selectedCard: {borderColor: '#bd93f9', borderWidth: 2},
claimedCard: {opacity: 0.5}, claimedCard: {opacity: 0.5},
disabledCard: {opacity: 0.45}, disabledCard: {opacity: 0.45},
disabledText: {color: '#9ca3af'}, disabledText: {color: '#6c7086'},
btnLabel: {color: '#ffffff', fontWeight: '600', fontSize: 13}, btnLabel: {color: '#cdd6f4', fontWeight: '600', fontSize: 13},
}); });
export default CacheCardItem; export default CacheCardItem;
+3 -8
View File
@@ -3,15 +3,11 @@ import {StyleSheet, Text, View} from 'react-native';
import {CACHE_CLAIM_TIMER} from '../../utils/geoMath'; import {CACHE_CLAIM_TIMER} from '../../utils/geoMath';
const ClaimTimerView = ({ cache, onClaimSuccess, isClaiming, showClaimedPopup }) => { const ClaimTimerView = ({ cache, onClaimSuccess, isClaiming, showClaimedPopup }) => {
// Initialisation -------------
// State ----------------------
const [timeLeft, setTimeLeft] = useState(CACHE_CLAIM_TIMER); const [timeLeft, setTimeLeft] = useState(CACHE_CLAIM_TIMER);
// Use a ref for the callback so it does not appear in the dependency array // Use a ref for the callback so it does not appear in the dependency array
const onClaimRef = useRef(onClaimSuccess); const onClaimRef = useRef(onClaimSuccess);
onClaimRef.current = onClaimSuccess; onClaimRef.current = onClaimSuccess;
// Handlers -------------------
useEffect(() => { useEffect(() => {
let timer; let timer;
@@ -25,7 +21,6 @@ const ClaimTimerView = ({ cache, onClaimSuccess, isClaiming, showClaimedPopup })
return () => clearTimeout(timer); return () => clearTimeout(timer);
}, [isClaiming, timeLeft, cache]); }, [isClaiming, timeLeft, cache]);
// View -----------------------
// Show "Cache Claimed!" popup after a successful claim // Show "Cache Claimed!" popup after a successful claim
if (showClaimedPopup) { if (showClaimedPopup) {
@@ -57,20 +52,20 @@ const styles = StyleSheet.create({
marginTop: 8, marginTop: 8,
}, },
text: { text: {
color: '#ffffff', color: '#cdd6f4',
fontSize: 16, fontSize: 16,
fontWeight: '700', fontWeight: '700',
}, },
claimedOverlay: { claimedOverlay: {
alignSelf: 'center', alignSelf: 'center',
backgroundColor: '#16a34a', backgroundColor: '#a6e3a1',
paddingHorizontal: 20, paddingHorizontal: 20,
paddingVertical: 14, paddingVertical: 14,
borderRadius: 10, borderRadius: 10,
marginTop: 8, marginTop: 8,
}, },
claimedText: { claimedText: {
color: '#ffffff', color: '#1e1e2e',
fontSize: 18, fontSize: 18,
fontWeight: '700', fontWeight: '700',
}, },
+22 -11
View File
@@ -3,17 +3,29 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context';
import {clearGameSession, getSession} from '../../hooks/SessionStore'; import {clearGameSession, getSession} from '../../hooks/SessionStore';
import API, {API_BASE_URL} from '../API/API'; import API, {API_BASE_URL} from '../API/API';
const BottomNavbar = ({navigation, routeName}) => { const BottomNavbar = ({navigation, routeName, routeParams}) => {
// Initialisation ------------ // Initialisation ------------
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const session = getSession(); const session = getSession();
const selectedDepartment = routeName === 'DepartmentSettingsScreen' && routeParams?.SGid
? {SGid: routeParams.SGid, SubGroupName: routeParams.SubGroupName || routeParams.selectedDepartmentName || 'Department'}
: null;
const adminTabs = [ const adminTabs = [
{label: 'Settings', route: 'GameSettingsScreen', onPress: () => navigation.navigate('GameSettingsScreen')}, {label: 'Settings', route: 'GameSettingsScreen', onPress: () => navigation.navigate('GameSettingsScreen')},
{label: 'Map', route: 'MapScreen', onPress: () => navigation.navigate('MapScreen')}, {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 // Only show the Leave tab when the player is actually in a game
@@ -78,8 +90,8 @@ const styles = StyleSheet.create({
container: { container: {
minHeight: 64, minHeight: 64,
borderTopWidth: 1, borderTopWidth: 1,
borderTopColor: '#555555', borderTopColor: '#45475a',
backgroundColor: '#4a4a4a', backgroundColor: '#313244',
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-around', justifyContent: 'space-around',
@@ -91,26 +103,25 @@ const styles = StyleSheet.create({
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
paddingVertical: 12, paddingVertical: 12,
borderRadius: 8,
marginHorizontal: 4, marginHorizontal: 4,
}, },
tabActive: { tabActive: {
backgroundColor: '#5c5c5c', backgroundColor: '#45475a',
}, },
tabLeave: { tabLeave: {
backgroundColor: 'rgba(220,38,38,0.15)', backgroundColor: 'rgba(243, 139, 168, 0.15)',
}, },
tabText: { tabText: {
color: '#d1d5db', color: '#bac2de',
fontSize: 12, fontSize: 12,
fontWeight: '600', fontWeight: '600',
textAlign: 'center', textAlign: 'center',
}, },
tabTextActive: { tabTextActive: {
color: '#ffffff', color: '#cdd6f4',
}, },
tabTextLeave: { tabTextLeave: {
color: '#fca5a5', color: '#D92800',
}, },
}); });
+4 -3
View File
@@ -17,6 +17,7 @@ const Screen = ({ children, style, showBack = false }) => {
TeamScreen: "Current Team", TeamScreen: "Current Team",
MapScreen: "Map", MapScreen: "Map",
GameSettingsScreen: "Game Settings", GameSettingsScreen: "Game Settings",
DepartmentSettingsScreen: "Department Settings",
PlayersScreen: "Players", PlayersScreen: "Players",
ExpandedMapScreen: "Map View", ExpandedMapScreen: "Map View",
}; };
@@ -34,7 +35,7 @@ const Screen = ({ children, style, showBack = false }) => {
<View style={[styles.screen, isAuthScreen && {paddingTop: insets.top, paddingBottom: insets.bottom}]}> <View style={[styles.screen, isAuthScreen && {paddingTop: insets.top, paddingBottom: insets.bottom}]}>
{!isAuthScreen && <TopNavbar title={pageTitle} showBack={showBack} navigation={navigation} />} {!isAuthScreen && <TopNavbar title={pageTitle} showBack={showBack} navigation={navigation} />}
<View style={[styles.content, style]}>{children}</View> <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" /> <StatusBar style="light" />
</View> </View>
); );
@@ -43,13 +44,13 @@ const Screen = ({ children, style, showBack = false }) => {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
screen: { screen: {
flex: 1, flex: 1,
backgroundColor: "#fff", backgroundColor: '#1e1e2e',
}, },
content: { content: {
flex: 1, flex: 1,
padding: 15, padding: 15,
zIndex: 1, zIndex: 1,
overflow: "hidden", overflow: 'hidden',
}, },
}); });
+2 -2
View File
@@ -47,12 +47,12 @@ const styles = StyleSheet.create({
container: { container: {
minHeight: 56, minHeight: 56,
borderBottomWidth: 1, borderBottomWidth: 1,
borderBottomColor: '#333333', borderBottomColor: '#45475a',
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
paddingHorizontal: 12, paddingHorizontal: 12,
backgroundColor: '#000000', backgroundColor: '#313244',
zIndex: 10, zIndex: 10,
}, },
leftSlot: { 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;
+315 -186
View File
@@ -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 {ActivityIndicator, Alert, ScrollView, StyleSheet, Switch, Text, TextInput, View} from 'react-native';
import {useFocusEffect} from '@react-navigation/native';
import Screen from '../layout/Screen'; import Screen from '../layout/Screen';
import Card from '../UI/Card'; import Card from '../UI/Card';
import {Button, ButtonTray} from '../UI/Button'; import {Button, ButtonTray} from '../UI/Button';
import useGameHook from '../../hooks/useGameHook'; import useGameHook from '../../hooks/useGameHook';
import {getSession, setSessionTeamsEnabled} from '../../hooks/SessionStore'; import {getSession, setSessionTeamsEnabled} from '../../hooks/SessionStore';
const GameSettingsScreen = () => { const GameSettingsScreen = ({navigation}) => {
// Initialisation ------------
const session = getSession(); const session = getSession();
const {getLobby, updateGroup, getSubgroups, createSubgroup, updateSubgroup, deleteSubgroup, resetGame, getAdminWaitlist, approveAdmin, rejectAdmin, getUser} = useGameHook(); const {getLobby, updateGroup, getSubgroups, createSubgroup, deleteSubgroup, resetGame, getAdminWaitlist, approveAdmin, rejectAdmin, getUser, disbandTeams} = useGameHook();
const isBusiness = Boolean(session.isBusiness); const gameApiRef = useRef({
getLobby,
// State ---------------------- getSubgroups,
getAdminWaitlist,
getUser,
});
gameApiRef.current = {
getLobby,
getSubgroups,
getAdminWaitlist,
getUser,
};
const isMountedRef = useRef(true);
const isRefreshingRef = useRef(false);
const hasHydratedRef = useRef(false);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [groupName, setGroupName] = useState(''); const [groupName, setGroupName] = useState('');
const [businessName, setBusinessName] = useState('');
const [teamsEnabled, setTeamsEnabled] = useState(session.teamsEnabled); const [teamsEnabled, setTeamsEnabled] = useState(session.teamsEnabled);
const [serverTeamsEnabled, setServerTeamsEnabled] = useState(session.teamsEnabled);
const [cacheTriggerMeters, setCacheTriggerMeters] = useState('20'); const [cacheTriggerMeters, setCacheTriggerMeters] = useState('20');
const [adminJoinCode, setAdminJoinCode] = useState(''); const [adminJoinCode, setAdminJoinCode] = useState('');
const [orgJoinCode, setOrgJoinCode] = useState('');
const [memberJoinCode, setMemberJoinCode] = useState(''); const [memberJoinCode, setMemberJoinCode] = useState('');
const [waitlist, setWaitlist] = useState([]); const [waitlist, setWaitlist] = useState([]);
const [waitlistNames, setWaitlistNames] = useState({}); const [waitlistNames, setWaitlistNames] = useState({});
// Business subgroup management const [isBusinessGroup, setIsBusinessGroup] = useState(Boolean(session.isBusiness));
const [subgroupList, setSubgroupList] = useState([]); const [subgroupList, setSubgroupList] = useState([]);
const [newDeptName, setNewDeptName] = useState(''); const [newDeptName, setNewDeptName] = useState('');
const [editingSGid, setEditingSGid] = useState(null);
const [editDeptTrigger, setEditDeptTrigger] = useState('20');
// Handlers ------------------- // Handlers -------------------
const loadWaitlist = async () => { useEffect(() => () => {
if (!session.currentGid) return; isMountedRef.current = false;
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);
};
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 || '');
};
useEffect(() => {
const load = async () => {
if (!session.currentGid) {
setLoading(false);
return;
}
const group = await getLobby(session.currentGid);
if (group) {
setGroupName(group.GroupName || '');
const serverTeams = Boolean(group.TeamsEnabled);
setTeamsEnabled(serverTeams);
// Keep session in sync so the toggle survives app reloads
setSessionTeamsEnabled(serverTeams);
setCacheTriggerMeters(String(group.CacheTriggerMeters || 20));
setAdminJoinCode(group.AdminJoinCode || '');
}
await loadSubgroups();
await loadWaitlist();
setLoading(false);
};
load();
}, []); }, []);
const handleSave = async () => { const refreshSettings = useCallback(async ({showLoader = false} = {}) => {
if (!session.currentGid) return; 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;
if (group) {
setGroupName(group.GroupName || '');
setBusinessName(group.BusinessOrSchoolName || '');
// Organisation mode
setIsBusinessGroup(Boolean(group.BusinessOrSchoolName));
const serverTeams = Boolean(group.TeamsEnabled);
setTeamsEnabled(serverTeams);
setServerTeamsEnabled(serverTeams);
setSessionTeamsEnabled(serverTeams);
setCacheTriggerMeters(String(group.CacheTriggerMeters || 20));
setAdminJoinCode(group.AdminJoinCode || '');
setOrgJoinCode(group.OrgJoinCode || '');
}
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;
}
}, []);
useFocusEffect(
useCallback(() => {
refreshSettings({showLoader: !hasHydratedRef.current});
}, [refreshSettings]),
);
// --- Individual save ---
const saveSettings = async () => {
setSaving(true); setSaving(true);
const result = await updateGroup(session.currentGid, { const result = await updateGroup(session.currentGid, {
GroupName: groupName.trim(), GroupName: groupName.trim(),
TeamsEnabled: teamsEnabled, TeamsEnabled: teamsEnabled,
CacheTriggerMeters: parseInt(cacheTriggerMeters) || 20, CacheTriggerMeters: parseInt(cacheTriggerMeters) || 20,
}); });
// Persist the new toggle value so the session reflects it after a reload if (result) {
if (result) setSessionTeamsEnabled(teamsEnabled); if (serverTeamsEnabled && !teamsEnabled) {
await disbandTeams(session.currentGid);
}
setSessionTeamsEnabled(teamsEnabled);
setServerTeamsEnabled(teamsEnabled);
}
setSaving(false); 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 = () => { const handleResetGame = () => {
Alert.alert('Reset Entire Game', 'This will remove ALL saved caches and reset progress for every player. Are you sure?', [ Alert.alert('Reset Entire Game', 'This will remove ALL saved caches and reset progress for every player. Are you sure?', [
{text: 'Cancel', style: 'cancel'}, {text: 'Cancel', style: 'cancel'},
@@ -105,32 +174,27 @@ const GameSettingsScreen = () => {
const handleApproveAdmin = async (entry) => { const handleApproveAdmin = async (entry) => {
await approveAdmin(entry.id); await approveAdmin(entry.id);
await loadWaitlist(); await refreshSettings();
}; };
const handleRejectAdmin = async (entry) => { const handleRejectAdmin = async (entry) => {
await rejectAdmin(entry.id); await rejectAdmin(entry.id);
await loadWaitlist(); await refreshSettings();
}; };
// Business — create a new department (subgroup) // Business — create a new department (subgroup)
const handleCreateDepartment = async () => { 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({ await createSubgroup({
SubGroupName: newDeptName.trim(), SubGroupName: name,
Gid: session.currentGid, Gid: session.currentGid,
IsAdminGroup: false, IsAdminGroup: false,
CacheTriggerMeters: parseInt(cacheTriggerMeters) || 20, CacheTriggerMeters: 20,
}); });
setNewDeptName(''); setNewDeptName('');
await loadSubgroups(); await refreshSettings();
};
// Business — save per-subgroup trigger distance
const handleSaveDeptTrigger = async (sgid) => {
await updateSubgroup(sgid, {CacheTriggerMeters: parseInt(editDeptTrigger) || 20});
setEditingSGid(null);
await loadSubgroups();
}; };
// Business — delete a department // Business — delete a department
@@ -142,12 +206,21 @@ const GameSettingsScreen = () => {
style: 'destructive', style: 'destructive',
onPress: async () => { onPress: async () => {
await deleteSubgroup(sg.SGid); 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 ----------------------- // View -----------------------
if (loading) { if (loading) {
@@ -169,6 +242,144 @@ const GameSettingsScreen = () => {
// Filter subgroups for the department list (non-admin only) // Filter subgroups for the department list (non-admin only)
const departments = subgroupList.filter((sg) => !sg.IsAdminGroup); 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 ( return (
<Screen> <Screen>
<ScrollView contentContainerStyle={styles.formContainer} showsVerticalScrollIndicator={false}> <ScrollView contentContainerStyle={styles.formContainer} showsVerticalScrollIndicator={false}>
@@ -209,7 +420,7 @@ const GameSettingsScreen = () => {
</View> </View>
<View style={styles.codeSection}> <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> <Text style={styles.codeValue}>{memberJoinCode || '—'}</Text>
</View> </View>
@@ -224,83 +435,6 @@ const GameSettingsScreen = () => {
</ButtonTray> </ButtonTray>
</View> </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 */} {/* Admin Waitlist */}
{waitlist.length > 0 && ( {waitlist.length > 0 && (
<View style={styles.waitlistSection}> <View style={styles.waitlistSection}>
@@ -348,18 +482,18 @@ const GameSettingsScreen = () => {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
center: {justifyContent: 'center', alignItems: 'center'}, center: {justifyContent: 'center', alignItems: 'center'},
formContainer: {paddingBottom: 20}, formContainer: {paddingBottom: 20},
sectionTitle: {fontSize: 20, fontWeight: '700', color: '#1f2937', marginBottom: 16}, sectionTitle: {fontSize: 20, fontWeight: '700', color: '#cdd6f4', marginBottom: 16},
label: {fontSize: 14, fontWeight: '600', color: '#374151', marginBottom: 6, marginTop: 14}, label: {fontSize: 14, fontWeight: '600', color: '#bac2de', marginBottom: 6, marginTop: 14},
body: {color: '#4b5563', fontSize: 15}, body: {color: '#bac2de', fontSize: 15},
input: { input: {
borderWidth: 1, borderWidth: 1,
borderColor: '#d1d5db', borderColor: '#45475a',
borderRadius: 8, borderRadius: 8,
paddingHorizontal: 12, paddingHorizontal: 12,
paddingVertical: 10, paddingVertical: 10,
fontSize: 16, fontSize: 16,
color: '#1f2937', color: '#cdd6f4',
backgroundColor: '#ffffff', backgroundColor: '#313244',
}, },
toggleRow: { toggleRow: {
flexDirection: 'row', flexDirection: 'row',
@@ -368,9 +502,9 @@ const styles = StyleSheet.create({
marginTop: 16, marginTop: 16,
paddingVertical: 10, paddingVertical: 10,
borderBottomWidth: 1, borderBottomWidth: 1,
borderBottomColor: '#e5e7eb', borderBottomColor: '#45475a',
}, },
toggleLabel: {fontSize: 14, fontWeight: '600', color: '#374151'}, toggleLabel: {fontSize: 14, fontWeight: '600', color: '#bac2de'},
codeSection: { codeSection: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
@@ -378,48 +512,43 @@ const styles = StyleSheet.create({
marginTop: 16, marginTop: 16,
paddingVertical: 12, paddingVertical: 12,
paddingHorizontal: 12, paddingHorizontal: 12,
backgroundColor: '#f3f4f6', backgroundColor: '#313244',
borderRadius: 8, borderRadius: 8,
}, },
codeLabel: {fontSize: 14, fontWeight: '600', color: '#374151'}, codeLabel: {fontSize: 14, fontWeight: '600', color: '#bac2de'},
codeValue: {fontSize: 16, fontWeight: '700', color: '#2563eb', letterSpacing: 2}, codeValue: {fontSize: 16, fontWeight: '700', color: '#bd93f9', letterSpacing: 2},
saveWrap: {marginTop: 24}, saveWrap: {marginTop: 24},
saveButton: {backgroundColor: '#16a34a', borderColor: '#16a34a'}, saveButton: {backgroundColor: '#a6e3a1', borderColor: '#a6e3a1'},
saveLabel: {color: '#ffffff', fontWeight: '600'}, saveLabel: {color: '#1e1e2e', fontWeight: '600'},
// Department / subgroup styles
deptSection: {marginTop: 30}, deptSection: {marginTop: 30},
deptRow: {flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start'}, deptIndexLabel: {fontSize: 14, fontWeight: '700', color: '#bac2de', marginBottom: 8},
deptInfo: {flex: 1}, deptCardRow: {flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8},
deptName: {fontSize: 15, fontWeight: '700', color: '#1f2937'}, deptCardInfo: {flex: 1, paddingRight: 10},
deptCode: {fontSize: 13, fontWeight: '600', color: '#2563eb', marginTop: 2, letterSpacing: 1}, deptCardActions: {flexDirection: 'row', gap: 6, flex: 0},
deptTrigger: {fontSize: 12, color: '#6b7280', marginTop: 2}, deptInfoLabel: {fontSize: 12, fontWeight: '600', color: '#6c7086', marginBottom: 2},
deptActions: {flexDirection: 'row', gap: 6}, deptName: {fontSize: 15, fontWeight: '700', color: '#cdd6f4'},
deptEditButton: {backgroundColor: '#2563eb', borderColor: '#2563eb', minHeight: 36, flex: 0, paddingHorizontal: 10}, deptCode: {fontSize: 13, fontWeight: '600', color: '#bd93f9', marginTop: 2, letterSpacing: 1},
deptDeleteButton: {backgroundColor: '#dc2626', borderColor: '#dc2626', minHeight: 36, flex: 0, paddingHorizontal: 10}, deptDeleteButton: {backgroundColor: '#D92800', borderColor: '#D92800', minHeight: 36, flex: 0, paddingHorizontal: 10},
deptBtnLabel: {color: '#ffffff', fontWeight: '600', fontSize: 13}, deptOpenButton: {backgroundColor: '#bd93f9', borderColor: '#bd93f9', minHeight: 36, flex: 0, paddingHorizontal: 10},
deptEditRow: {marginTop: 10, paddingTop: 10, borderTopWidth: 1, borderTopColor: '#e5e7eb'}, deptBtnLabel: {color: '#1e1e2e', fontWeight: '600', fontSize: 13},
deptSaveButton: {backgroundColor: '#16a34a', borderColor: '#16a34a'},
deptCancelButton: {backgroundColor: '#6b7280', borderColor: '#6b7280'},
createDeptRow: {flexDirection: 'row', gap: 10, marginTop: 12, alignItems: 'center'}, createDeptRow: {flexDirection: 'row', gap: 10, marginTop: 12, alignItems: 'center'},
createDeptButton: {backgroundColor: '#16a34a', borderColor: '#16a34a', flex: 0, paddingHorizontal: 16}, createDeptButton: {backgroundColor: '#a6e3a1', borderColor: '#a6e3a1', flex: 0, paddingHorizontal: 16},
createDeptLabel: {color: '#ffffff', fontWeight: '600'}, createDeptLabel: {color: '#1e1e2e', fontWeight: '600'},
emptyText: {color: '#9ca3af', textAlign: 'center', marginTop: 10, fontSize: 14}, emptyText: {color: '#6c7086', textAlign: 'center', marginTop: 10, fontSize: 14},
// Admin waitlist styles
waitlistSection: {marginTop: 30}, waitlistSection: {marginTop: 30},
waitlistRow: { waitlistRow: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center', 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}, waitlistActions: {flexDirection: 'row', gap: 6},
approveButton: {backgroundColor: '#16a34a', borderColor: '#16a34a', minHeight: 36, flex: 0, paddingHorizontal: 10}, approveButton: {backgroundColor: '#a6e3a1', borderColor: '#a6e3a1', minHeight: 36, flex: 0, paddingHorizontal: 10},
approveBtnLabel: {color: '#ffffff', fontWeight: '600', fontSize: 13}, approveBtnLabel: {color: '#1e1e2e', fontWeight: '600', fontSize: 13},
rejectButton: {backgroundColor: '#dc2626', borderColor: '#dc2626', minHeight: 36, flex: 0, paddingHorizontal: 10}, rejectButton: {backgroundColor: '#D92800', borderColor: '#D92800', minHeight: 36, flex: 0, paddingHorizontal: 10},
rejectBtnLabel: {color: '#ffffff', fontWeight: '600', fontSize: 13}, rejectBtnLabel: {color: '#1e1e2e', fontWeight: '600', fontSize: 13},
// Reset game styles
resetSection: {marginTop: 30}, resetSection: {marginTop: 30},
resetButton: {backgroundColor: '#dc2626', borderColor: '#dc2626'}, resetButton: {backgroundColor: '#D92800', borderColor: '#D92800'},
resetLabel: {color: '#ffffff', fontWeight: '600'}, resetLabel: {color: '#ffffff', fontWeight: '600'},
}); });
+106 -33
View File
@@ -6,12 +6,14 @@ import {Button} from '../UI/Button';
import useGameHook from '../../hooks/useGameHook'; import useGameHook from '../../hooks/useGameHook';
import {getSession} from '../../hooks/SessionStore'; import {getSession} from '../../hooks/SessionStore';
const LeaderboardScreen = () => { const LeaderboardScreen = ({navigation, route}) => {
// Initialisation ------------ // Initialisation ------------
const session = getSession(); const session = getSession();
const isAdmin = session.isAcceptedAdmin; 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 ---------------------- // State ----------------------
@@ -23,6 +25,7 @@ const LeaderboardScreen = () => {
const [myTeamRanking, setMyTeamRanking] = useState([]); const [myTeamRanking, setMyTeamRanking] = useState([]);
const [expandedTeam, setExpandedTeam] = useState(null); const [expandedTeam, setExpandedTeam] = useState(null);
const [totalCacheCount, setTotalCacheCount] = useState(0); const [totalCacheCount, setTotalCacheCount] = useState(0);
const [departmentList, setDepartmentList] = useState([]);
// Handlers ------------------- // Handlers -------------------
@@ -30,16 +33,32 @@ const LeaderboardScreen = () => {
if (!session.currentGid) { setLoading(false); return; } if (!session.currentGid) { setLoading(false); return; }
const group = await getLobby(session.currentGid); 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); 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; const total = (caches || []).length;
setTotalCacheCount(total); setTotalCacheCount(total);
if (isTeams) { if (isTeams) {
// Build team rankings // Build team rankings
const teams = await getTeams(session.currentGid); const teams = await getTeams(session.currentGid, effectiveSGid ?? null);
const teamData = []; const teamData = [];
for (const team of (teams || [])) { for (const team of (teams || [])) {
@@ -80,9 +99,9 @@ const LeaderboardScreen = () => {
} }
} else { } else {
// Build player rankings (no teams) // Build player rankings (no teams)
const allMembers = await getGroupMembers(session.currentGid); const scopedMembers = await getGroupMembers(session.currentGid, effectiveSGid ?? null);
const playerData = []; 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 user = await getUser(m.Uid);
const count = (caches || []).filter((c) => const count = (caches || []).filter((c) =>
(c.Claims || []).some((cl) => cl.Uid === m.Uid) (c.Claims || []).some((cl) => cl.Uid === m.Uid)
@@ -98,7 +117,7 @@ const LeaderboardScreen = () => {
} }
setLoading(false); setLoading(false);
}, [session.currentGid, session.currentTid]); }, [session.currentGid, session.currentTid, session.currentSGid, isBusiness, isAdmin, selectedDepartment, selectedDepartment?.SGid]);
useEffect(() => { loadData(); }, [loadData]); 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 ----------------------- // View -----------------------
if (loading) { 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) // Helper: render an expandable team card (admin version with delete + reset)
const renderAdminTeamCard = (team, index) => ( const renderAdminTeamCard = (team, index) => (
<Card key={team.tid}> <Card key={team.tid}>
@@ -179,11 +239,17 @@ const LeaderboardScreen = () => {
</View> </View>
))} ))}
<View style={styles.deleteTeamWrap}> <View style={styles.deleteTeamWrap}>
<Button
label="Reset Team Progress"
onClick={() => handleResetTeam(team)}
styleButton={styles.resetTeamButton}
styleLabel={styles.deleteTeamLabel}
/>
<Button <Button
label="Delete Team" label="Delete Team"
onClick={() => handleDeleteTeam(team)} onClick={() => handleDeleteTeam(team)}
styleButton={styles.deleteTeamButton} styleButton={styles.deleteTeamButton}
styleLabel={styles.deleteTeamLabel} styleLabel={styles.deleteTeamDeleteLabel}
/> />
</View> </View>
</View> </View>
@@ -248,7 +314,10 @@ const LeaderboardScreen = () => {
// Teams enabled: show team list (no tabs for admin) // Teams enabled: show team list (no tabs for admin)
if (teamsEnabled) { if (teamsEnabled) {
return ( return (
<Screen style={styles.container}> <Screen style={styles.container} showBack={Boolean(selectedDepartment)}>
{selectedDepartment && (
<Text style={styles.departmentTitle}>{selectedDepartment.SubGroupName}</Text>
)}
<ScrollView style={styles.listSection}> <ScrollView style={styles.listSection}>
{teamRankings.map((team, index) => renderAdminTeamCard(team, index))} {teamRankings.map((team, index) => renderAdminTeamCard(team, index))}
{teamRankings.length === 0 && ( {teamRankings.length === 0 && (
@@ -261,7 +330,10 @@ const LeaderboardScreen = () => {
// Teams disabled: show player list with reset buttons // Teams disabled: show player list with reset buttons
return ( return (
<Screen style={styles.container}> <Screen style={styles.container} showBack={Boolean(selectedDepartment)}>
{selectedDepartment && (
<Text style={styles.departmentTitle}>{selectedDepartment.SubGroupName}</Text>
)}
<ScrollView style={styles.listSection}> <ScrollView style={styles.listSection}>
{playerRankings.map((player, index) => renderAdminPlayerCard(player, index))} {playerRankings.map((player, index) => renderAdminPlayerCard(player, index))}
{playerRankings.length === 0 && ( {playerRankings.length === 0 && (
@@ -339,11 +411,13 @@ const LeaderboardScreen = () => {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
center: {justifyContent: 'center', alignItems: 'center'}, center: {justifyContent: 'center', alignItems: 'center'},
container: {padding: 0}, 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: { tabRow: {
flexDirection: 'row', flexDirection: 'row',
borderBottomWidth: 1, borderBottomWidth: 1,
borderBottomColor: '#e5e7eb', borderBottomColor: '#45475a',
}, },
tabButton: { tabButton: {
flex: 1, flex: 1,
@@ -352,46 +426,45 @@ const styles = StyleSheet.create({
}, },
tabButtonActive: { tabButtonActive: {
borderBottomWidth: 2, borderBottomWidth: 2,
borderBottomColor: '#2563eb', borderBottomColor: '#bd93f9',
}, },
tabLabel: {fontSize: 15, fontWeight: '600', color: '#6b7280'}, tabLabel: {fontSize: 15, fontWeight: '600', color: '#6c7086'},
tabLabelActive: {color: '#2563eb'}, tabLabelActive: {color: '#bd93f9'},
listSection: {flex: 1, paddingHorizontal: 15, paddingTop: 10}, listSection: {flex: 1, paddingHorizontal: 15, paddingTop: 10},
rankRow: { rankRow: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
gap: 10, gap: 10,
}, },
rank: {fontSize: 16, fontWeight: '700', color: '#374151', width: 32}, rank: {fontSize: 16, fontWeight: '700', color: '#bac2de', width: 32},
rankName: {flex: 1, fontSize: 15, fontWeight: '600', color: '#1f2937'}, rankName: {flex: 1, fontSize: 15, fontWeight: '600', color: '#cdd6f4'},
score: {fontSize: 14, fontWeight: '600', color: '#16a34a'}, score: {fontSize: 14, fontWeight: '600', color: '#a6e3a1'},
subRow: { subRow: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
paddingLeft: 42, paddingLeft: 42,
paddingVertical: 6, paddingVertical: 6,
borderTopWidth: 1, borderTopWidth: 1,
borderTopColor: '#f3f4f6', borderTopColor: '#45475a',
}, },
subName: {fontSize: 13, color: '#4b5563'}, subName: {fontSize: 13, color: '#bac2de'},
subScore: {fontSize: 13, fontWeight: '600', color: '#16a34a', marginRight: 8}, subScore: {fontSize: 13, fontWeight: '600', color: '#a6e3a1', marginRight: 8},
emptyText: {color: '#9ca3af', textAlign: 'center', marginTop: 30, fontSize: 14}, emptyText: {color: '#6c7086', textAlign: 'center', marginTop: 30, fontSize: 14},
// Admin delete team button deleteTeamWrap: {marginTop: 8, paddingLeft: 42, flexDirection: 'row', gap: 8},
deleteTeamWrap: {marginTop: 8, paddingLeft: 42}, resetTeamButton: {backgroundColor: '#f4a460', borderColor: '#f4a460', minHeight: 36, flex: 1},
deleteTeamButton: {backgroundColor: '#dc2626', borderColor: '#dc2626', minHeight: 36}, deleteTeamButton: {backgroundColor: '#D92800', borderColor: '#D92800', minHeight: 36, flex: 1},
deleteTeamLabel: {color: '#ffffff', fontWeight: '600', fontSize: 13}, deleteTeamLabel: {color: '#1e1e2e', fontWeight: '600', fontSize: 13},
// Admin reset mini button (inline) deleteTeamDeleteLabel: {color: '#ffffff', fontWeight: '600', fontSize: 13},
resetMiniButton: { resetMiniButton: {
backgroundColor: '#f59e0b', backgroundColor: '#86efac',
borderRadius: 6, borderRadius: 6,
paddingHorizontal: 10, paddingHorizontal: 10,
paddingVertical: 4, paddingVertical: 4,
marginLeft: 6, marginLeft: 6,
}, },
resetMiniLabel: {color: '#ffffff', fontWeight: '600', fontSize: 12}, resetMiniLabel: {color: '#1e1e2e', fontWeight: '600', fontSize: 12},
// Current user self-highlight (light green card / sub-row) selfCard: {backgroundColor: 'rgba(166, 227, 161, 0.15)', borderColor: '#a6e3a1'},
selfCard: {backgroundColor: '#dcfce7', borderColor: '#86efac'}, selfSubRow: {backgroundColor: 'rgba(166, 227, 161, 0.15)'},
selfSubRow: {backgroundColor: '#dcfce7'},
}); });
export default LeaderboardScreen; export default LeaderboardScreen;
+11 -19
View File
@@ -5,16 +5,9 @@ import Screen from '../layout/Screen';
import {Button, ButtonTray} from '../UI/Button'; import {Button, ButtonTray} from '../UI/Button';
const LoginScreen = ({ navigation }) => { const LoginScreen = ({ navigation }) => {
// Initialisation -------------
const { email, password, error, isLoading, setEmail, setPassword, login } = useAuthHook(); const { email, password, error, isLoading, setEmail, setPassword, login } = useAuthHook();
// State ----------------------
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
// Handlers -------------------
const handleLogin = async () => { const handleLogin = async () => {
setSubmitting(true); setSubmitting(true);
const user = await login(); const user = await login();
@@ -29,8 +22,6 @@ const LoginScreen = ({ navigation }) => {
navigation.reset({ index: 0, routes: [{ name: 'Game' }] }); navigation.reset({ index: 0, routes: [{ name: 'Game' }] });
}; };
// View -----------------------
return ( return (
<Screen style={styles.container}> <Screen style={styles.container}>
<View style={styles.content}> <View style={styles.content}>
@@ -39,7 +30,7 @@ const LoginScreen = ({ navigation }) => {
<TextInput <TextInput
style={styles.input} style={styles.input}
placeholder="Email" placeholder="Email"
placeholderTextColor="#9ca3af" placeholderTextColor="#6c7086"
autoCapitalize="none" autoCapitalize="none"
keyboardType="email-address" keyboardType="email-address"
value={email} value={email}
@@ -49,7 +40,7 @@ const LoginScreen = ({ navigation }) => {
<TextInput <TextInput
style={styles.input} style={styles.input}
placeholder="Password" placeholder="Password"
placeholderTextColor="#9ca3af" placeholderTextColor="#6c7086"
secureTextEntry secureTextEntry
value={password} value={password}
onChangeText={setPassword} onChangeText={setPassword}
@@ -87,33 +78,34 @@ const styles = StyleSheet.create({
fontWeight: 'bold', fontWeight: 'bold',
textAlign: 'center', textAlign: 'center',
marginBottom: 10, marginBottom: 10,
color: '#cdd6f4',
}, },
input: { input: {
borderWidth: 1, borderWidth: 1,
borderColor: '#d1d5db', borderColor: '#45475a',
borderRadius: 8, borderRadius: 8,
paddingHorizontal: 12, paddingHorizontal: 12,
paddingVertical: 10, paddingVertical: 10,
fontSize: 16, fontSize: 16,
color: '#1f2937', color: '#cdd6f4',
backgroundColor: '#ffffff', backgroundColor: '#313244',
}, },
errorText: { errorText: {
color: '#dc2626', color: '#D92800',
textAlign: 'center', textAlign: 'center',
marginTop: 5, marginTop: 5,
}, },
submitButton: { submitButton: {
backgroundColor: '#2563eb', backgroundColor: '#bd93f9',
borderColor: '#2563eb', borderColor: '#bd93f9',
}, },
submitText: { submitText: {
color: '#ffffff', color: '#1e1e2e',
fontWeight: '600', fontWeight: '600',
}, },
linkText: { linkText: {
textAlign: 'center', textAlign: 'center',
color: '#2563eb', color: '#bd93f9',
fontWeight: '600', fontWeight: '600',
paddingVertical: 10, paddingVertical: 10,
}, },
+270 -41
View File
@@ -16,17 +16,14 @@ import {getFovCone} from '../../utils/geoMath';
const DEFAULT_REGION = {latitude: 51.5074, longitude: -0.1278, latitudeDelta: 0.05, longitudeDelta: 0.05}; const DEFAULT_REGION = {latitude: 51.5074, longitude: -0.1278, latitudeDelta: 0.05, longitudeDelta: 0.05};
const MapScreen = ({navigation}) => { const MapScreen = ({navigation, route}) => {
// Initialisation ------------
const session = getSession(); const session = getSession();
const {getCaches, claimCache, upsertCache, deleteCache, joinPrivateGame, createPrivateGame, getLobby, getUser, getSubgroups} = useGameHook(); const {getCaches, claimCache, upsertCache, deleteCache, joinPrivateGame, createPrivateGame, getLobby, getUser, getSubgroups, getGroupByOrgCode} = useGameHook();
// Stable ref to prevent infinite re-render loops // Stable ref to prevent infinite re-render loops
const getCachesRef = useRef(getCaches); const getCachesRef = useRef(getCaches);
getCachesRef.current = getCaches; getCachesRef.current = getCaches;
const lastAppliedRouteDepartmentRef = useRef(null);
// State ----------------------
const [inGame, setInGame] = useState(Boolean(session.currentGid)); const [inGame, setInGame] = useState(Boolean(session.currentGid));
const [isAdmin, setIsAdmin] = useState(session.isAcceptedAdmin); const [isAdmin, setIsAdmin] = useState(session.isAcceptedAdmin);
@@ -41,12 +38,21 @@ const MapScreen = ({navigation}) => {
const [claimedPopupVisible, setClaimedPopupVisible] = useState(false); const [claimedPopupVisible, setClaimedPopupVisible] = useState(false);
const [subgroups, setSubgroups] = useState([]); const [subgroups, setSubgroups] = useState([]);
// 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('');
// Admin cache creation / editing state // Admin cache creation / editing state
const [isCreating, setIsCreating] = useState(false); const [isCreating, setIsCreating] = useState(false);
const [editingCacheId, setEditingCacheId] = useState(null); const [editingCacheId, setEditingCacheId] = useState(null);
const [newCoord, setNewCoord] = useState(null); const [newCoord, setNewCoord] = useState(null);
const [newName, setNewName] = useState(''); const [newName, setNewName] = useState('');
const [newClue, setNewClue] = useState(''); const [newClue, setNewClue] = useState('');
const [selectedCacheSubgroupId, setSelectedCacheSubgroupId] = useState(null);
const [departmentDropdownOpen, setDepartmentDropdownOpen] = useState(false);
const claimDistance = groupInfo?.CacheTriggerMeters || 20; const claimDistance = groupInfo?.CacheTriggerMeters || 20;
const isPlayer = inGame && !isAdmin; const isPlayer = inGame && !isAdmin;
@@ -77,8 +83,6 @@ const MapScreen = ({navigation}) => {
? {...userLocation, latitudeDelta: 0.01, longitudeDelta: 0.01} ? {...userLocation, latitudeDelta: 0.01, longitudeDelta: 0.01}
: DEFAULT_REGION; : DEFAULT_REGION;
// Handlers -------------------
useEffect(() => { useEffect(() => {
if (!session.currentGid) return; if (!session.currentGid) return;
getLobby(session.currentGid).then(setGroupInfo); getLobby(session.currentGid).then(setGroupInfo);
@@ -86,7 +90,36 @@ const MapScreen = ({navigation}) => {
}, [inGame]); }, [inGame]);
// Default member subgroup SGid for cache creation (first non-admin subgroup) // Default member subgroup SGid for cache creation (first non-admin subgroup)
const defaultMemberSGid = subgroups.find((sg) => !sg.IsAdminGroup)?.SGid || null; 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 (!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;
}
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 loadCaches = useCallback(async () => {
const currentGid = getSession().currentGid; 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) => { const handleClaim = useCallback(async (cacheId) => {
if (!session.currentGid) return; if (!session.currentGid) return;
const result = await claimCache({ const result = await claimCache({
@@ -212,6 +303,8 @@ const MapScreen = ({navigation}) => {
setNewCoord(fallback); setNewCoord(fallback);
setNewName(''); setNewName('');
setNewClue(''); setNewClue('');
if (!selectedCacheSubgroupId && defaultMemberSGid) setSelectedCacheSubgroupId(defaultMemberSGid);
setDepartmentDropdownOpen(false);
setIsCreating(true); setIsCreating(true);
}; };
@@ -220,6 +313,8 @@ const MapScreen = ({navigation}) => {
setNewCoord(cache.coordinates); setNewCoord(cache.coordinates);
setNewName(cache.name || ''); setNewName(cache.name || '');
setNewClue(cache.clue || ''); setNewClue(cache.clue || '');
setSelectedCacheSubgroupId(cache.subgroupId || defaultMemberSGid);
setDepartmentDropdownOpen(false);
setIsCreating(true); setIsCreating(true);
}; };
@@ -231,24 +326,28 @@ const MapScreen = ({navigation}) => {
const handleSaveCache = async () => { const handleSaveCache = async () => {
if (!newCoord || !newClue.trim() || !session.currentGid) return; if (!newCoord || !newClue.trim() || !session.currentGid) return;
const subgroupId = selectedCacheSubgroupId || defaultMemberSGid;
if (!subgroupId) return;
const payload = { const payload = {
gid: session.currentGid, gid: session.currentGid,
name: newName.trim(), name: newName.trim(),
latitude: newCoord.latitude, latitude: newCoord.latitude,
longitude: newCoord.longitude, longitude: newCoord.longitude,
clue: newClue.trim(), clue: newClue.trim(),
subgroupId: defaultMemberSGid, subgroupId,
}; };
if (editingCacheId) payload.cacheId = editingCacheId; if (editingCacheId) payload.cacheId = editingCacheId;
await upsertCache(payload); await upsertCache(payload);
setIsCreating(false); setIsCreating(false);
setEditingCacheId(null); setEditingCacheId(null);
setDepartmentDropdownOpen(false);
await loadCaches(); await loadCaches();
}; };
const handleCancelCreate = () => { const handleCancelCreate = () => {
setIsCreating(false); setIsCreating(false);
setEditingCacheId(null); setEditingCacheId(null);
setDepartmentDropdownOpen(false);
}; };
const handleExpandMap = () => { const handleExpandMap = () => {
@@ -273,9 +372,86 @@ const MapScreen = ({navigation}) => {
return claims.some((c) => c.Uid === session.currentUid); return claims.some((c) => c.Uid === session.currentUid);
}; };
const showAdminDepartmentLabel = Boolean(isAdmin && groupInfo?.BusinessOrSchoolName);
// View ----------------------- // 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) { if (!inGame) {
return ( return (
<Screen style={styles.center}> <Screen style={styles.center}>
@@ -355,6 +531,32 @@ const MapScreen = ({navigation}) => {
</MapView> </MapView>
</View> </View>
<ScrollView style={styles.formSection}> <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 <TextInput
style={styles.formInput} style={styles.formInput}
placeholder="Cache Name" placeholder="Cache Name"
@@ -462,6 +664,7 @@ const MapScreen = ({navigation}) => {
key={cache.id} key={cache.id}
cache={cache} cache={cache}
isAdmin={true} isAdmin={true}
departmentName={showAdminDepartmentLabel ? (departmentNamesById[String(cache.subgroupId)] || 'Unassigned') : null}
onEdit={handleEditCache} onEdit={handleEditCache}
onDelete={handleDeleteCache} onDelete={handleDeleteCache}
/> />
@@ -548,27 +751,31 @@ const styles = StyleSheet.create({
center: {justifyContent: 'center', alignItems: 'center'}, center: {justifyContent: 'center', alignItems: 'center'},
containerMap: {padding: 0}, containerMap: {padding: 0},
mapContainer: {height: 200}, mapContainer: {height: 200},
error: {color: '#dc2626', fontSize: 15}, error: {color: '#D92800', fontSize: 15},
loadingText: {color: '#6b7280', fontSize: 14, marginTop: 10}, loadingText: {color: '#6c7086', fontSize: 14, marginTop: 10},
inputRow: {flexDirection: 'row', gap: 10, marginBottom: 15, width: '100%', paddingHorizontal: 20}, inputRow: {flexDirection: 'row', gap: 10, marginBottom: 15, width: '100%', paddingHorizontal: 20},
fullRow: {width: '100%', paddingHorizontal: 20}, fullRow: {width: '100%', paddingHorizontal: 20},
codeInput: { codeInput: {
flex: 1, flex: 1,
borderWidth: 1, borderWidth: 1,
borderColor: '#d1d5db', borderColor: '#45475a',
borderRadius: 8, borderRadius: 8,
paddingHorizontal: 12, paddingHorizontal: 12,
paddingVertical: 10, paddingVertical: 10,
fontSize: 16, fontSize: 16,
color: '#1f2937', color: '#cdd6f4',
backgroundColor: '#ffffff', backgroundColor: '#313244',
}, }, joinButton: {backgroundColor: '#bd93f9', borderColor: '#bd93f9', flex: 0, paddingHorizontal: 20},
joinButton: {backgroundColor: '#2563eb', borderColor: '#2563eb', flex: 0, paddingHorizontal: 20}, joinLabel: {color: '#1e1e2e', fontWeight: '600'},
joinLabel: {color: '#ffffff', fontWeight: '600'}, createButton: {backgroundColor: '#a6e3a1', borderColor: '#a6e3a1'},
createButton: {backgroundColor: '#16a34a', borderColor: '#16a34a'}, createLabel: {color: '#1e1e2e', fontWeight: '600'},
createLabel: {color: '#ffffff', 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: { warning: {
color: '#dc2626', color: '#D92800',
fontWeight: 'bold', fontWeight: 'bold',
textAlign: 'center', textAlign: 'center',
paddingVertical: 8, paddingVertical: 8,
@@ -586,42 +793,64 @@ const styles = StyleSheet.create({
justifyContent: 'center', justifyContent: 'center',
zIndex: 10, zIndex: 10,
}, },
expandIcon: {color: '#ffffff', fontSize: 18, fontWeight: '700'}, expandIcon: {color: '#cdd6f4', fontSize: 18, fontWeight: '700'},
// Admin create button + scrollable cache list
createCacheWrap: { createCacheWrap: {
paddingHorizontal: 12, paddingHorizontal: 12,
paddingVertical: 10, paddingVertical: 10,
backgroundColor: '#ffffff', backgroundColor: '#313244',
borderBottomWidth: 1, borderBottomWidth: 1,
borderBottomColor: '#e5e7eb', borderBottomColor: '#45475a',
}, },
createCacheButton: {backgroundColor: '#2563eb', borderColor: '#2563eb', flex: 0}, createCacheButton: {backgroundColor: '#bd93f9', borderColor: '#bd93f9', flex: 0},
createCacheLabel: {color: '#ffffff', fontWeight: '600'}, createCacheLabel: {color: '#1e1e2e', fontWeight: '600'},
cacheListWrap: {flex: 1, backgroundColor: '#f3f4f6'}, cacheListWrap: {flex: 1, backgroundColor: '#1e1e2e'},
cacheListContent: {paddingHorizontal: 12, paddingTop: 10, paddingBottom: 12}, cacheListContent: {paddingHorizontal: 12, paddingTop: 10, paddingBottom: 12},
// Cache editor form
formSection: {flex: 1, paddingHorizontal: 12, paddingVertical: 10}, formSection: {flex: 1, paddingHorizontal: 12, paddingVertical: 10},
formInput: { formInput: {
borderWidth: 1, borderWidth: 1,
borderColor: '#d1d5db', borderColor: '#45475a',
borderRadius: 8, borderRadius: 8,
paddingHorizontal: 12, paddingHorizontal: 12,
paddingVertical: 10, paddingVertical: 10,
fontSize: 16, fontSize: 16,
color: '#1f2937', color: '#cdd6f4',
backgroundColor: '#ffffff', backgroundColor: '#313244',
marginBottom: 10, 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}, coordRow: {flexDirection: 'row', gap: 10},
coordField: {flex: 1}, coordField: {flex: 1},
coordLabel: {fontSize: 12, fontWeight: '600', color: '#6b7280', marginBottom: 4}, coordLabel: {fontSize: 12, fontWeight: '600', color: '#6c7086', marginBottom: 4},
saveButton: {backgroundColor: '#16a34a', borderColor: '#16a34a'}, saveButton: {backgroundColor: '#a6e3a1', borderColor: '#a6e3a1'},
saveLabel: {color: '#ffffff', fontWeight: '600'}, saveLabel: {color: '#1e1e2e', fontWeight: '600'},
cancelButton: {backgroundColor: '#6b7280', borderColor: '#6b7280'}, cancelButton: {backgroundColor: '#6c7086', borderColor: '#6c7086'},
cancelLabel: {color: '#ffffff', fontWeight: '600'}, cancelLabel: {color: '#cdd6f4', fontWeight: '600'},
// Player cache section cacheSection: {flex: 1, backgroundColor: '#1e1e2e', paddingHorizontal: 12, paddingTop: 10},
cacheSection: {flex: 1, backgroundColor: '#f3f4f6', paddingHorizontal: 12, paddingTop: 10}, emptyText: {color: '#6c7086', textAlign: 'center', marginTop: 20, fontSize: 14},
emptyText: {color: '#9ca3af', textAlign: 'center', marginTop: 20, fontSize: 14},
mapWrap: {flex: 1}, mapWrap: {flex: 1},
}); });
+84 -24
View File
@@ -1,32 +1,57 @@
import React, {useCallback, useEffect, useState} from 'react'; 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 Screen from '../layout/Screen';
import Card from '../UI/Card'; import Card from '../UI/Card';
import {Button} from '../UI/Button'; import {Button} from '../UI/Button';
import useGameHook from '../../hooks/useGameHook'; import useGameHook from '../../hooks/useGameHook';
import {getSession} from '../../hooks/SessionStore'; import {getSession} from '../../hooks/SessionStore';
const PlayersScreen = () => { const PlayersScreen = ({navigation, route}) => {
// Initialisation ------------ // Initialisation ------------
const session = getSession(); 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 ---------------------- // State ----------------------
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [players, setPlayers] = useState([]); const [players, setPlayers] = useState([]);
const [departmentList, setDepartmentList] = useState([]);
// Handlers ------------------- // Handlers -------------------
const loadData = useCallback(async () => { const loadData = useCallback(async () => {
if (!session.currentGid) { setLoading(false); return; } 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) // Get ALL members (including admins)
const allMembers = await getGroupMembers(session.currentGid); const allMembers = await getGroupMembers(session.currentGid, effectiveSGid ?? null);
// Get admin waitlist entries // 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 // 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. // 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; }); .filter((m) => { if (seenUids.has(m.Uid)) return false; seenUids.add(m.Uid); return true; });
// Build team lookup: { Uid -> { teamCode, teamName, isLeader } } // Build team lookup: { Uid -> { teamCode, teamName, isLeader } }
const allTeams = await getTeams(session.currentGid); const allTeams = await getTeams(session.currentGid, effectiveSGid ?? null);
const teamInfoByUid = {}; const teamInfoByUid = {};
for (const team of (allTeams || [])) { for (const team of (allTeams || [])) {
const tms = await getTeamMembers(team.Tid); const tms = await getTeamMembers(team.Tid);
@@ -73,6 +98,7 @@ const PlayersScreen = () => {
team: teamInfoByUid[m.Uid] || null, team: teamInfoByUid[m.Uid] || null,
adminTag, adminTag,
waitlistId, waitlistId,
departmentName: subgroupNameMap[m.SGid] || null,
}); });
} }
} }
@@ -89,13 +115,14 @@ const PlayersScreen = () => {
team: null, team: null,
adminTag: '[Admin Awaiting Response]', adminTag: '[Admin Awaiting Response]',
waitlistId: w.id, waitlistId: w.id,
departmentName: null,
}); });
} }
} }
setPlayers(playerList); setPlayers(playerList);
setLoading(false); setLoading(false);
}, [session.currentGid]); }, [session.currentGid, session.currentUid, session.currentSGid, isBusiness, isAdmin, selectedDepartment, selectedDepartment?.SGid]);
useEffect(() => { loadData(); }, [loadData]); useEffect(() => { loadData(); }, [loadData]);
@@ -152,8 +179,34 @@ const PlayersScreen = () => {
); );
} }
if (isBusiness && isAdmin && !selectedDepartment) {
return ( return (
<Screen style={styles.container}> <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} showBack={Boolean(selectedDepartment)}>
<Text style={styles.sectionTitle}>Players</Text> <Text style={styles.sectionTitle}>Players</Text>
<ScrollView style={styles.listSection}> <ScrollView style={styles.listSection}>
{players.map((player) => ( {players.map((player) => (
@@ -167,6 +220,12 @@ const PlayersScreen = () => {
{' '}{player.adminTag} {' '}{player.adminTag}
</Text> </Text>
)} )}
{/* Show department name in brackets for non-admin players (org accounts) */}
{!player.adminTag && player.departmentName && (
<Text style={styles.departmentName}>
{' '}({player.departmentName})
</Text>
)}
</Text> </Text>
<View style={styles.playerMeta}> <View style={styles.playerMeta}>
{player.team?.isLeader && ( {player.team?.isLeader && (
@@ -209,7 +268,7 @@ const PlayersScreen = () => {
label="Kick" label="Kick"
onClick={() => handleRemovePlayer(player.membershipId)} onClick={() => handleRemovePlayer(player.membershipId)}
styleButton={styles.removeButton} styleButton={styles.removeButton}
styleLabel={styles.actionLabel} styleLabel={styles.kickLabel}
/> />
)} )}
</> </>
@@ -228,38 +287,39 @@ const PlayersScreen = () => {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
center: {justifyContent: 'center', alignItems: 'center', flex: 1}, center: {justifyContent: 'center', alignItems: 'center', flex: 1},
container: {padding: 0}, container: {padding: 0},
body: {color: '#4b5563', fontSize: 15}, body: {color: '#bac2de', fontSize: 15},
sectionTitle: { sectionTitle: {
fontSize: 20, fontSize: 20,
fontWeight: '700', fontWeight: '700',
color: '#1f2937', color: '#cdd6f4',
marginBottom: 12, marginBottom: 12,
paddingHorizontal: 15, paddingHorizontal: 15,
paddingTop: 15, paddingTop: 15,
}, },
openLabel: {fontSize: 13, fontWeight: '700', color: '#bd93f9'},
listSection: {flex: 1, paddingHorizontal: 15}, listSection: {flex: 1, paddingHorizontal: 15},
emptyText: {color: '#9ca3af', textAlign: 'center', marginTop: 30, fontSize: 14}, emptyText: {color: '#6c7086', textAlign: 'center', marginTop: 30, fontSize: 14},
playerRow: { playerRow: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
gap: 8, gap: 8,
marginBottom: 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}, playerMeta: {flexDirection: 'row', alignItems: 'center', gap: 6},
leaderBadge: {fontSize: 11, color: '#2563eb', fontWeight: '700'}, leaderBadge: {fontSize: 11, color: '#bd93f9', fontWeight: '700'},
teamCodeBadge: {fontSize: 12, fontWeight: '700', color: '#16a34a', letterSpacing: 1}, teamCodeBadge: {fontSize: 12, fontWeight: '700', color: '#a6e3a1', letterSpacing: 1},
adminBadge: {fontSize: 12, fontWeight: '700', color: '#dc2626'}, adminBadge: {fontSize: 12, fontWeight: '700', color: '#D92800'},
waitlistBadge: {fontSize: 11, fontWeight: '600', color: '#f59e0b'}, waitlistBadge: {fontSize: 11, fontWeight: '600', color: '#f4a460'},
actionRow: {flexDirection: 'row', gap: 8}, actionRow: {flexDirection: 'row', gap: 8},
resetButton: {backgroundColor: '#f59e0b', borderColor: '#f59e0b', minHeight: 36, flex: 1, paddingHorizontal: 10}, resetButton: {backgroundColor: '#86efac', borderColor: '#86efac', minHeight: 36, flex: 1, paddingHorizontal: 10},
removeButton: {backgroundColor: '#dc2626', borderColor: '#dc2626', minHeight: 36, flex: 1, paddingHorizontal: 10}, removeButton: {backgroundColor: '#D92800', borderColor: '#D92800', minHeight: 36, flex: 1, paddingHorizontal: 10},
actionLabel: {color: '#ffffff', fontWeight: '600', fontSize: 13}, actionLabel: {color: '#1e1e2e', fontWeight: '600', fontSize: 13},
// Waitlisted admin card tint — light red so they read as pending, not regular players kickLabel: {color: '#ffffff', fontWeight: '600', fontSize: 13},
waitlistCard: {backgroundColor: '#FFD1DC', borderColor: '#fca5a5'}, departmentName: {fontSize: 14, color: '#6c7086', fontStyle: 'italic'},
// Approve / deny buttons for waitlisted admins waitlistCard: {backgroundColor: 'rgba(217, 40, 0, 0.15)', borderColor: '#D92800'},
approveButton: {backgroundColor: '#16a34a', borderColor: '#16a34a', minHeight: 36, flex: 1, paddingHorizontal: 10}, approveButton: {backgroundColor: '#a6e3a1', borderColor: '#a6e3a1', minHeight: 36, flex: 1, paddingHorizontal: 10},
denyButton: {backgroundColor: '#dc2626', borderColor: '#dc2626', minHeight: 36, flex: 1, paddingHorizontal: 10}, denyButton: {backgroundColor: '#D92800', borderColor: '#D92800', minHeight: 36, flex: 1, paddingHorizontal: 10},
}); });
export default PlayersScreen; export default PlayersScreen;
+18 -25
View File
@@ -5,8 +5,6 @@ import Screen from '../layout/Screen';
import {Button, ButtonTray} from '../UI/Button'; import {Button, ButtonTray} from '../UI/Button';
const RegisterScreen = ({navigation}) => { const RegisterScreen = ({navigation}) => {
// Initialisation ------------
const { const {
username, email, password, confirmPassword, accountType, username, email, password, confirmPassword, accountType,
error, isLoading, error, isLoading,
@@ -14,12 +12,8 @@ const RegisterScreen = ({navigation}) => {
register, register,
} = useAuthHook(); } = useAuthHook();
// State ----------------------
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
// Handlers -------------------
const handleRegister = async () => { const handleRegister = async () => {
setSubmitting(true); setSubmitting(true);
const user = await register(); const user = await register();
@@ -32,8 +26,6 @@ const RegisterScreen = ({navigation}) => {
navigation.reset({index: 0, routes: [{name: 'Game'}]}); navigation.reset({index: 0, routes: [{name: 'Game'}]});
}; };
// View -----------------------
return ( return (
<Screen style={styles.container}> <Screen style={styles.container}>
<View style={styles.content}> <View style={styles.content}>
@@ -42,7 +34,7 @@ const RegisterScreen = ({navigation}) => {
<TextInput <TextInput
style={styles.input} style={styles.input}
placeholder="Username" placeholder="Username"
placeholderTextColor="#9ca3af" placeholderTextColor="#6c7086"
value={username} value={username}
onChangeText={setUsername} onChangeText={setUsername}
editable={!isLoading} editable={!isLoading}
@@ -50,7 +42,7 @@ const RegisterScreen = ({navigation}) => {
<TextInput <TextInput
style={styles.input} style={styles.input}
placeholder="Email" placeholder="Email"
placeholderTextColor="#9ca3af" placeholderTextColor="#6c7086"
autoCapitalize="none" autoCapitalize="none"
keyboardType="email-address" keyboardType="email-address"
value={email} value={email}
@@ -60,7 +52,7 @@ const RegisterScreen = ({navigation}) => {
<TextInput <TextInput
style={styles.input} style={styles.input}
placeholder="Password" placeholder="Password"
placeholderTextColor="#9ca3af" placeholderTextColor="#6c7086"
secureTextEntry secureTextEntry
value={password} value={password}
onChangeText={setPassword} onChangeText={setPassword}
@@ -69,7 +61,7 @@ const RegisterScreen = ({navigation}) => {
<TextInput <TextInput
style={styles.input} style={styles.input}
placeholder="Confirm Password" placeholder="Confirm Password"
placeholderTextColor="#9ca3af" placeholderTextColor="#6c7086"
secureTextEntry secureTextEntry
value={confirmPassword} value={confirmPassword}
onChangeText={setConfirmPassword} onChangeText={setConfirmPassword}
@@ -128,16 +120,17 @@ const styles = StyleSheet.create({
fontWeight: 'bold', fontWeight: 'bold',
textAlign: 'center', textAlign: 'center',
marginBottom: 10, marginBottom: 10,
color: '#cdd6f4',
}, },
input: { input: {
borderWidth: 1, borderWidth: 1,
borderColor: '#d1d5db', borderColor: '#45475a',
borderRadius: 8, borderRadius: 8,
paddingHorizontal: 12, paddingHorizontal: 12,
paddingVertical: 10, paddingVertical: 10,
fontSize: 16, fontSize: 16,
color: '#1f2937', color: '#cdd6f4',
backgroundColor: '#ffffff', backgroundColor: '#313244',
}, },
typeRow: { typeRow: {
flexDirection: 'row', flexDirection: 'row',
@@ -146,38 +139,38 @@ const styles = StyleSheet.create({
typeButton: { typeButton: {
flex: 1, flex: 1,
borderWidth: 1, borderWidth: 1,
borderColor: '#cbd5e1', borderColor: '#45475a',
borderRadius: 8, borderRadius: 8,
alignItems: 'center', alignItems: 'center',
paddingVertical: 10, paddingVertical: 10,
}, },
typeButtonActive: { typeButtonActive: {
backgroundColor: '#2563eb', backgroundColor: '#bd93f9',
borderColor: '#2563eb', borderColor: '#bd93f9',
}, },
typeText: { typeText: {
color: '#1f2937', color: '#bac2de',
fontWeight: '600', fontWeight: '600',
}, },
typeTextActive: { typeTextActive: {
color: '#ffffff', color: '#1e1e2e',
}, },
errorText: { errorText: {
color: '#dc2626', color: '#D92800',
textAlign: 'center', textAlign: 'center',
marginTop: 5, marginTop: 5,
}, },
submitButton: { submitButton: {
backgroundColor: '#2563eb', backgroundColor: '#bd93f9',
borderColor: '#2563eb', borderColor: '#bd93f9',
}, },
submitText: { submitText: {
color: '#ffffff', color: '#1e1e2e',
fontWeight: '600', fontWeight: '600',
}, },
linkText: { linkText: {
textAlign: 'center', textAlign: 'center',
color: '#2563eb', color: '#bd93f9',
fontWeight: '600', fontWeight: '600',
paddingVertical: 10, paddingVertical: 10,
}, },
+71 -44
View File
@@ -7,13 +7,10 @@ import useGameHook from '../../hooks/useGameHook';
import {getSession, setSessionTeam} from '../../hooks/SessionStore'; import {getSession, setSessionTeam} from '../../hooks/SessionStore';
const TeamScreen = () => { const TeamScreen = () => {
// Initialisation ------------
const session = getSession(); const session = getSession();
const isAdmin = session.isAcceptedAdmin; const isAdmin = session.isAcceptedAdmin;
const {getTeam, createTeam, joinTeamByCode, getTeamMembers, leaveTeam, getUser, updateUser, updateTeam, getAdminWaitlist} = useGameHook(); const isBusiness = Boolean(session.isBusiness);
const {getLobby, getSubgroup, getTeam, createTeam, joinTeamByCode, getTeamMembers, leaveTeam, getUser, updateUser, updateTeam, getAdminWaitlist} = useGameHook();
// State ----------------------
const [activeTid, setActiveTid] = useState(session.currentTid); const [activeTid, setActiveTid] = useState(session.currentTid);
const [teamCode, setTeamCode] = useState(''); const [teamCode, setTeamCode] = useState('');
@@ -21,12 +18,11 @@ const TeamScreen = () => {
const [members, setMembers] = useState([]); const [members, setMembers] = useState([]);
const [memberNames, setMemberNames] = useState({}); const [memberNames, setMemberNames] = useState({});
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [teamsDisabled, setTeamsDisabled] = useState(false);
const [onAdminWaitlist, setOnAdminWaitlist] = useState(false); const [onAdminWaitlist, setOnAdminWaitlist] = useState(false);
const [editingName, setEditingName] = useState(false); const [editingName, setEditingName] = useState(false);
const [teamNameDraft, setTeamNameDraft] = useState(''); const [teamNameDraft, setTeamNameDraft] = useState('');
// Handlers -------------------
const loadTeamData = async (tid) => { const loadTeamData = async (tid) => {
const t = await getTeam(tid); const t = await getTeam(tid);
setTeam(t); setTeam(t);
@@ -42,8 +38,24 @@ const TeamScreen = () => {
useEffect(() => { useEffect(() => {
const load = async () => { const load = async () => {
// Check if the user is on the admin waitlist if (!session.currentGid) { setLoading(false); return; }
if (session.currentGid && !activeTid) {
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); const waitlist = await getAdminWaitlist(session.currentGid, session.currentUid);
if (waitlist.length > 0) { if (waitlist.length > 0) {
setOnAdminWaitlist(true); setOnAdminWaitlist(true);
@@ -61,7 +73,11 @@ const TeamScreen = () => {
if (!teamCode.trim()) return; if (!teamCode.trim()) return;
// Force uppercase just in case a lowercase code is pasted // Force uppercase just in case a lowercase code is pasted
const code = teamCode.trim().toUpperCase(); 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 (!result) return;
// If the server indicates this code matched the admin join code // 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 // Waiting for admin approval
if (onAdminWaitlist) { if (onAdminWaitlist) {
return ( return (
@@ -270,64 +297,65 @@ const TeamScreen = () => {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
center: {justifyContent: 'center', alignItems: 'center'}, center: {justifyContent: 'center', alignItems: 'center'},
container: {padding: 0}, container: {padding: 0},
// Shown when the user has not yet joined a game disabledWrap: {paddingHorizontal: 30, alignItems: 'center', opacity: 0.45},
notInGameText: {color: '#6b7280', fontSize: 15, fontWeight: '600', marginBottom: 16, textAlign: 'center', paddingHorizontal: 20}, disabledTitle: {fontSize: 20, fontWeight: '700', color: '#ffffff', marginBottom: 10, textAlign: 'center'},
inputDisabled: {backgroundColor: '#f3f4f6', opacity: 0.5}, 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}, inputRow: {flexDirection: 'row', gap: 10, marginBottom: 15, width: '100%', paddingHorizontal: 20},
fullRow: {width: '100%', paddingHorizontal: 20}, fullRow: {width: '100%', paddingHorizontal: 20},
codeInput: { codeInput: {
flex: 1, flex: 1,
borderWidth: 1, borderWidth: 1,
borderColor: '#d1d5db', borderColor: '#45475a',
borderRadius: 8, borderRadius: 8,
paddingHorizontal: 12, paddingHorizontal: 12,
paddingVertical: 10, paddingVertical: 10,
fontSize: 16, fontSize: 16,
color: '#1f2937', color: '#ffffff',
backgroundColor: '#ffffff', backgroundColor: '#313244',
}, },
joinButton: {backgroundColor: '#2563eb', borderColor: '#2563eb', flex: 0, paddingHorizontal: 20}, joinButton: {backgroundColor: '#bd93f9', borderColor: '#bd93f9', flex: 0, paddingHorizontal: 20},
joinLabel: {color: '#ffffff', fontWeight: '600'}, joinLabel: {color: '#1e1e2e', fontWeight: '600'},
createButton: {backgroundColor: '#16a34a', borderColor: '#16a34a'}, createButton: {backgroundColor: '#a6e3a1', borderColor: '#a6e3a1'},
createLabel: {color: '#ffffff', fontWeight: '600'}, createLabel: {color: '#1e1e2e', fontWeight: '600'},
waitlistText: { waitlistText: {
fontSize: 18, fontSize: 18,
fontWeight: '700', fontWeight: '700',
color: '#374151', color: '#ffffff',
textAlign: 'center', textAlign: 'center',
paddingHorizontal: 30, paddingHorizontal: 30,
}, },
teamHeader: { teamHeader: {
backgroundColor: '#374151', backgroundColor: '#45475a',
paddingVertical: 14, paddingVertical: 14,
paddingHorizontal: 16, paddingHorizontal: 16,
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
gap: 8, 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}, teamCodeValue: {color: '#ffffff', fontSize: 17, fontWeight: '700', letterSpacing: 2},
// Team name / rename section teamNameSection: {paddingHorizontal: 15, paddingVertical: 10, borderBottomWidth: 1, borderBottomColor: '#45475a'},
teamNameSection: {paddingHorizontal: 15, paddingVertical: 10, borderBottomWidth: 1, borderBottomColor: '#e5e7eb'}, teamNameText: {fontSize: 18, fontWeight: '700', color: '#ffffff', flex: 1},
teamNameText: {fontSize: 18, fontWeight: '700', color: '#1f2937', flex: 1},
renameRow: {flexDirection: 'row', alignItems: 'center', gap: 8}, renameRow: {flexDirection: 'row', alignItems: 'center', gap: 8},
renameInput: { renameInput: {
flex: 1, flex: 1,
borderWidth: 1, borderWidth: 1,
borderColor: '#d1d5db', borderColor: '#45475a',
borderRadius: 8, borderRadius: 8,
paddingHorizontal: 12, paddingHorizontal: 12,
paddingVertical: 8, paddingVertical: 8,
fontSize: 16, fontSize: 16,
color: '#1f2937', color: '#ffffff',
backgroundColor: '#ffffff', backgroundColor: '#313244',
}, },
renameButton: {backgroundColor: '#2563eb', borderColor: '#2563eb', flex: 0, minHeight: 36, paddingHorizontal: 14}, renameButton: {backgroundColor: '#bd93f9', borderColor: '#bd93f9', flex: 0, minHeight: 36, paddingHorizontal: 14},
renameLabel: {color: '#ffffff', fontWeight: '600', fontSize: 13}, renameLabel: {color: '#1e1e2e', fontWeight: '600', fontSize: 13},
renameSaveButton: {backgroundColor: '#16a34a', borderColor: '#16a34a', flex: 0, minHeight: 36, paddingHorizontal: 12}, renameSaveButton: {backgroundColor: '#a6e3a1', borderColor: '#a6e3a1', flex: 0, minHeight: 36, paddingHorizontal: 12},
renameSaveLabel: {color: '#ffffff', fontWeight: '600', fontSize: 13}, renameSaveLabel: {color: '#1e1e2e', fontWeight: '600', fontSize: 13},
renameCancelButton: {backgroundColor: '#6b7280', borderColor: '#6b7280', flex: 0, minHeight: 36, paddingHorizontal: 12}, renameCancelButton: {backgroundColor: '#6c7086', borderColor: '#6c7086', flex: 0, minHeight: 36, paddingHorizontal: 12},
renameCancelLabel: {color: '#ffffff', fontWeight: '600', fontSize: 13}, renameCancelLabel: {color: '#cdd6f4', fontWeight: '600', fontSize: 13},
memberList: {flex: 1, paddingHorizontal: 15, paddingTop: 10}, memberList: {flex: 1, paddingHorizontal: 15, paddingTop: 10},
memberRow: { memberRow: {
flexDirection: 'row', flexDirection: 'row',
@@ -335,16 +363,15 @@ const styles = StyleSheet.create({
alignItems: 'center', alignItems: 'center',
}, },
memberInfo: {flex: 1, flexDirection: 'row', alignItems: 'center', gap: 6}, memberInfo: {flex: 1, flexDirection: 'row', alignItems: 'center', gap: 6},
memberName: {fontSize: 16, color: '#1f2937', fontWeight: '600'}, memberName: {fontSize: 16, color: '#ffffff', fontWeight: '600'},
leaderBadge: {fontSize: 12, color: '#2563eb', fontWeight: '700'}, leaderBadge: {fontSize: 12, color: '#bd93f9', fontWeight: '700'},
kickButton: {backgroundColor: '#dc2626', borderColor: '#dc2626', minHeight: 36, flex: 0, paddingHorizontal: 14}, kickButton: {backgroundColor: '#D92800', borderColor: '#D92800', minHeight: 36, flex: 0, paddingHorizontal: 14},
kickLabel: {color: '#ffffff', fontWeight: '600', fontSize: 13}, kickLabel: {color: '#1e1e2e', fontWeight: '600', fontSize: 13},
emptyText: {color: '#9ca3af', textAlign: 'center', marginTop: 30, fontSize: 14}, emptyText: {color: '#ffffff', textAlign: 'center', marginTop: 30, fontSize: 14},
leaveWrap: {paddingHorizontal: 0, paddingTop: 10, paddingBottom: 16}, leaveWrap: {paddingHorizontal: 0, paddingTop: 10, paddingBottom: 16},
leaveButton: {backgroundColor: '#dc2626', borderColor: '#dc2626'}, leaveButton: {backgroundColor: '#D92800', borderColor: '#D92800'},
leaveLabel: {color: '#ffffff', fontWeight: '600'}, leaveLabel: {color: '#1e1e2e', fontWeight: '600'},
// Current user self-highlight (light green card) selfCard: {backgroundColor: 'rgba(166, 227, 161, 0.15)', borderColor: '#a6e3a1'},
selfCard: {backgroundColor: '#dcfce7', borderColor: '#86efac'},
}); });
export default TeamScreen; export default TeamScreen;
+52 -9
View File
@@ -41,6 +41,15 @@ const useGameHook = () => {
return response.isSuccess ? response.result : null; 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 // Subgroups
const getSubgroups = async (gid) => { const getSubgroups = async (gid) => {
const response = await API.get(`${subgroupsEndpoint}?Gid=${gid}`); const response = await API.get(`${subgroupsEndpoint}?Gid=${gid}`);
@@ -69,12 +78,21 @@ const useGameHook = () => {
// Subgroup members // Subgroup members
const joinPrivateGame = async (payload) => { 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; return response.isSuccess ? response.result : null;
}; };
const getGroupMembers = async (gid) => { const getGroupMembers = async (gid, sgid = null) => {
const response = await API.get(`${subgroupMembersEndpoint}?Gid=${gid}`); let url = `${subgroupMembersEndpoint}?Gid=${gid}`;
if (sgid !== null && sgid !== undefined) url += `&SGid=${sgid}`;
const response = await API.get(url);
return response.isSuccess ? response.result : []; return response.isSuccess ? response.result : [];
}; };
@@ -84,8 +102,10 @@ const useGameHook = () => {
}; };
// Teams // Teams
const getTeams = async (gid) => { const getTeams = async (gid, sgid = null) => {
const response = await API.get(`${teamsEndpoint}?Gid=${gid}`); let url = `${teamsEndpoint}?Gid=${gid}`;
if (sgid !== null && sgid !== undefined) url += `&SGid=${sgid}`;
const response = await API.get(url);
return response.isSuccess ? response.result : []; return response.isSuccess ? response.result : [];
}; };
@@ -109,8 +129,21 @@ const useGameHook = () => {
return response.isSuccess; return response.isSuccess;
}; };
const disbandTeams = async (gid) => {
const response = await API.post(`${teamsEndpoint}/disband?Gid=${gid}`, {});
return response.isSuccess;
};
const joinTeamByCode = async (payload) => { 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; return response.isSuccess ? response.result : null;
}; };
@@ -163,9 +196,11 @@ const useGameHook = () => {
return response.isSuccess; return response.isSuccess;
}; };
// Game reset — remove all caches for a game // Game reset — remove all caches for a game (optionally scoped to a subgroup)
const resetGame = async (gid) => { const resetGame = async (gid, sgid = null) => {
const response = await API.post(`${cachesEndpoint}/reset?Gid=${gid}`, {}); let url = `${cachesEndpoint}/reset?Gid=${gid}`;
if (sgid !== null) url += `&SGid=${sgid}`;
const response = await API.post(url, {});
return response.isSuccess; return response.isSuccess;
}; };
@@ -175,6 +210,11 @@ const useGameHook = () => {
return response.isSuccess; return response.isSuccess;
}; };
const resetTeamProgress = async (gid, tid) => {
const response = await API.post(`${cachesEndpoint}/reset-team/${tid}?Gid=${gid}`, {});
return response.isSuccess;
};
// Admin waitlist // Admin waitlist
const getAdminWaitlist = async (gid, uid = null) => { const getAdminWaitlist = async (gid, uid = null) => {
let url = `${adminWaitlistEndpoint}?Gid=${gid}`; let url = `${adminWaitlistEndpoint}?Gid=${gid}`;
@@ -201,6 +241,7 @@ const useGameHook = () => {
createPrivateGame, createPrivateGame,
getLobby, getLobby,
updateGroup, updateGroup,
getGroupByOrgCode,
getSubgroups, getSubgroups,
getSubgroup, getSubgroup,
createSubgroup, createSubgroup,
@@ -214,6 +255,7 @@ const useGameHook = () => {
createTeam, createTeam,
updateTeam, updateTeam,
deleteTeam, deleteTeam,
disbandTeams,
joinTeamByCode, joinTeamByCode,
getTeamMembers, getTeamMembers,
leaveTeam, leaveTeam,
@@ -223,6 +265,7 @@ const useGameHook = () => {
deleteCache, deleteCache,
resetGame, resetGame,
resetPlayerProgress, resetPlayerProgress,
resetTeamProgress,
getAdminWaitlist, getAdminWaitlist,
approveAdmin, approveAdmin,
rejectAdmin, rejectAdmin,