diff --git a/src/components/layout/BottomNavbar.js b/src/components/layout/BottomNavbar.js
index 3838578..4090cb6 100644
--- a/src/components/layout/BottomNavbar.js
+++ b/src/components/layout/BottomNavbar.js
@@ -3,17 +3,29 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context';
import {clearGameSession, getSession} from '../../hooks/SessionStore';
import API, {API_BASE_URL} from '../API/API';
-const BottomNavbar = ({navigation, routeName}) => {
+const BottomNavbar = ({navigation, routeName, routeParams}) => {
// Initialisation ------------
const insets = useSafeAreaInsets();
const session = getSession();
+ const selectedDepartment = routeName === 'DepartmentSettingsScreen' && routeParams?.SGid
+ ? {SGid: routeParams.SGid, SubGroupName: routeParams.SubGroupName || routeParams.selectedDepartmentName || 'Department'}
+ : null;
+
const adminTabs = [
{label: 'Settings', route: 'GameSettingsScreen', onPress: () => navigation.navigate('GameSettingsScreen')},
{label: 'Map', route: 'MapScreen', onPress: () => navigation.navigate('MapScreen')},
- {label: 'Players', route: 'PlayersScreen', onPress: () => navigation.navigate('PlayersScreen')},
- {label: 'Leaderboard', route: 'LeaderboardScreen', onPress: () => navigation.navigate('LeaderboardScreen')},
+ {
+ label: 'Players',
+ route: 'PlayersScreen',
+ onPress: () => navigation.navigate('PlayersScreen', selectedDepartment ? {department: selectedDepartment} : undefined),
+ },
+ {
+ label: 'Leaderboard',
+ route: 'LeaderboardScreen',
+ onPress: () => navigation.navigate('LeaderboardScreen', selectedDepartment ? {department: selectedDepartment} : undefined),
+ },
];
// Only show the Leave tab when the player is actually in a game
diff --git a/src/components/layout/Screen.js b/src/components/layout/Screen.js
index 3d9b491..a73bdf7 100644
--- a/src/components/layout/Screen.js
+++ b/src/components/layout/Screen.js
@@ -35,7 +35,7 @@ const Screen = ({ children, style, showBack = false }) => {
{!isAuthScreen && }
{children}
- {!isAuthScreen && }
+ {!isAuthScreen && }
);
diff --git a/src/components/screens/GameSettingsScreen.js b/src/components/screens/GameSettingsScreen.js
index cbc071f..a9dd01e 100644
--- a/src/components/screens/GameSettingsScreen.js
+++ b/src/components/screens/GameSettingsScreen.js
@@ -1,5 +1,6 @@
-import React, {useEffect, useState} from 'react';
+import React, {useCallback, useState} from 'react';
import {ActivityIndicator, Alert, ScrollView, StyleSheet, Switch, Text, TextInput, View} from 'react-native';
+import {useFocusEffect} from '@react-navigation/native';
import Screen from '../layout/Screen';
import Card from '../UI/Card';
import {Button, ButtonTray} from '../UI/Button';
@@ -11,7 +12,6 @@ const GameSettingsScreen = ({navigation}) => {
const session = getSession();
const {getLobby, updateGroup, getSubgroups, createSubgroup, deleteSubgroup, resetGame, getAdminWaitlist, approveAdmin, rejectAdmin, getUser, disbandTeams} = useGameHook();
- const isBusiness = Boolean(session.isBusiness);
// State ----------------------
@@ -27,15 +27,16 @@ const GameSettingsScreen = ({navigation}) => {
const [memberJoinCode, setMemberJoinCode] = useState('');
const [waitlist, setWaitlist] = useState([]);
const [waitlistNames, setWaitlistNames] = useState({});
+ const [isBusinessGroup, setIsBusinessGroup] = useState(Boolean(session.isBusiness));
// Business subgroup management
const [subgroupList, setSubgroupList] = useState([]);
const [newDeptName, setNewDeptName] = useState('');
// Handlers -------------------
- const loadWaitlist = async () => {
- if (!session.currentGid) return;
- const rows = await getAdminWaitlist(session.currentGid);
+ const loadWaitlist = useCallback(async (gid) => {
+ if (!gid) return;
+ const rows = await getAdminWaitlist(gid);
setWaitlist(rows || []);
const names = {};
for (const entry of (rows || [])) {
@@ -43,27 +44,28 @@ const GameSettingsScreen = ({navigation}) => {
if (u) names[entry.Uid] = u.username;
}
setWaitlistNames(names);
- };
+ }, [getAdminWaitlist, getUser]);
- const loadSubgroups = async () => {
- if (!session.currentGid) return;
- const sgs = await getSubgroups(session.currentGid);
+ const loadSubgroups = useCallback(async (gid) => {
+ if (!gid) return;
+ const sgs = await getSubgroups(gid);
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 || '');
- };
+ }, [getSubgroups]);
- useEffect(() => {
- const load = async () => {
- if (!session.currentGid) {
+ const loadSettings = useCallback(async () => {
+ const activeSession = getSession();
+ if (!activeSession.currentGid) {
setLoading(false);
return;
}
- const group = await getLobby(session.currentGid);
+ const group = await getLobby(activeSession.currentGid);
if (group) {
setGroupName(group.GroupName || '');
setBusinessName(group.BusinessOrSchoolName || '');
+ setIsBusinessGroup(Boolean(group.BusinessOrSchoolName || group.OrgJoinCode));
const serverTeams = Boolean(group.TeamsEnabled);
setTeamsEnabled(serverTeams);
setServerTeamsEnabled(serverTeams);
@@ -72,12 +74,17 @@ const GameSettingsScreen = ({navigation}) => {
setAdminJoinCode(group.AdminJoinCode || '');
setOrgJoinCode(group.OrgJoinCode || '');
}
- await loadSubgroups();
- await loadWaitlist();
+ await loadSubgroups(activeSession.currentGid);
+ await loadWaitlist(activeSession.currentGid);
setLoading(false);
- };
- load();
- }, []);
+ }, [getLobby, loadSubgroups, loadWaitlist]);
+
+ useFocusEffect(
+ useCallback(() => {
+ setLoading(true);
+ loadSettings();
+ }, [loadSettings]),
+ );
// --- Individual save ---
const saveSettings = async () => {
@@ -100,7 +107,7 @@ const GameSettingsScreen = ({navigation}) => {
const handleSave = async () => {
if (!session.currentGid) return;
// Business save — only save business name
- if (isBusiness) {
+ if (isBusinessGroup) {
setSaving(true);
await updateGroup(session.currentGid, {
BusinessOrSchoolName: businessName.trim(),
@@ -180,7 +187,11 @@ const GameSettingsScreen = ({navigation}) => {
// Business — navigate to department settings
const handleEnterDepartment = (sg) => {
- navigation.navigate('DepartmentSettingsScreen', {SGid: sg.SGid, Gid: session.currentGid});
+ navigation.navigate('DepartmentSettingsScreen', {
+ SGid: sg.SGid,
+ Gid: session.currentGid,
+ SubGroupName: sg.SubGroupName,
+ });
};
// View -----------------------
@@ -205,7 +216,7 @@ const GameSettingsScreen = ({navigation}) => {
const departments = subgroupList.filter((sg) => !sg.IsAdminGroup);
// ===== Business Admin View =====
- if (isBusiness) {
+ if (isBusinessGroup) {
return (
diff --git a/src/components/screens/LeaderboardScreen.js b/src/components/screens/LeaderboardScreen.js
index 2b93636..c30b5fa 100644
--- a/src/components/screens/LeaderboardScreen.js
+++ b/src/components/screens/LeaderboardScreen.js
@@ -13,7 +13,7 @@ const LeaderboardScreen = ({navigation, route}) => {
const isAdmin = session.isAcceptedAdmin;
const isBusiness = Boolean(session.isBusiness);
const selectedDepartment = route?.params?.department || null;
- const {getLobby, getTeams, getTeamMembers, getGroupMembers, getCaches, getUser, deleteTeam, resetPlayerProgress, resetTeamProgress, getSubgroups} = useGameHook();
+ const {getLobby, getSubgroup, getTeams, getTeamMembers, getGroupMembers, getCaches, getUser, deleteTeam, resetPlayerProgress, resetTeamProgress, getSubgroups} = useGameHook();
// State ----------------------
@@ -33,7 +33,16 @@ const LeaderboardScreen = ({navigation, route}) => {
if (!session.currentGid) { setLoading(false); return; }
const group = await getLobby(session.currentGid);
- const isTeams = Boolean(group?.TeamsEnabled);
+
+ const effectiveSGid = isBusiness
+ ? ((isAdmin && selectedDepartment?.SGid) ? selectedDepartment.SGid : (isAdmin ? null : session.currentSGid))
+ : null;
+
+ let isTeams = Boolean(group?.TeamsEnabled);
+ if (effectiveSGid !== null && effectiveSGid !== undefined) {
+ const subgroup = await getSubgroup(effectiveSGid);
+ if (subgroup) isTeams = Boolean(subgroup.TeamsEnabled);
+ }
setTeamsEnabled(isTeams);
if (isBusiness && isAdmin && !selectedDepartment) {
@@ -43,24 +52,16 @@ const LeaderboardScreen = ({navigation, route}) => {
return;
}
- const leaderboardSGid = (isBusiness && isAdmin && selectedDepartment?.SGid)
- ? selectedDepartment.SGid
- : null;
-
- const caches = await getCaches(session.currentGid, leaderboardSGid);
+ const caches = await getCaches(session.currentGid, effectiveSGid ?? null);
const total = (caches || []).length;
setTotalCacheCount(total);
if (isTeams) {
// Build team rankings
- const teams = await getTeams(session.currentGid);
+ const teams = await getTeams(session.currentGid, effectiveSGid ?? null);
const teamData = [];
- const scopedTeams = leaderboardSGid
- ? (teams || []).filter((team) => !team.SGid || team.SGid === leaderboardSGid)
- : (teams || []);
-
- for (const team of scopedTeams) {
+ for (const team of (teams || [])) {
const members = await getTeamMembers(team.Tid);
const memberData = [];
for (const m of (members || [])) {
@@ -98,12 +99,9 @@ const LeaderboardScreen = ({navigation, route}) => {
}
} else {
// Build player rankings (no teams)
- const allMembers = await getGroupMembers(session.currentGid);
- const scopedMembers = leaderboardSGid
- ? (allMembers || []).filter((mem) => mem.SGid === leaderboardSGid)
- : (allMembers || []);
+ const scopedMembers = await getGroupMembers(session.currentGid, effectiveSGid ?? null);
const playerData = [];
- for (const m of scopedMembers.filter((mem) => !mem.IsAcceptedAdmin)) {
+ for (const m of (scopedMembers || []).filter((mem) => !mem.IsAcceptedAdmin)) {
const user = await getUser(m.Uid);
const count = (caches || []).filter((c) =>
(c.Claims || []).some((cl) => cl.Uid === m.Uid)
@@ -119,7 +117,7 @@ const LeaderboardScreen = ({navigation, route}) => {
}
setLoading(false);
- }, [session.currentGid, session.currentTid, isBusiness, isAdmin, selectedDepartment, selectedDepartment?.SGid]);
+ }, [session.currentGid, session.currentTid, session.currentSGid, isBusiness, isAdmin, selectedDepartment, selectedDepartment?.SGid]);
useEffect(() => { loadData(); }, [loadData]);
diff --git a/src/components/screens/PlayersScreen.js b/src/components/screens/PlayersScreen.js
index c4aec28..b6e0a9a 100644
--- a/src/components/screens/PlayersScreen.js
+++ b/src/components/screens/PlayersScreen.js
@@ -1,28 +1,43 @@
import React, {useCallback, useEffect, useState} from 'react';
-import {ActivityIndicator, Alert, ScrollView, StyleSheet, Text, View} from 'react-native';
+import {ActivityIndicator, Alert, Pressable, ScrollView, StyleSheet, Text, View} from 'react-native';
import Screen from '../layout/Screen';
import Card from '../UI/Card';
import {Button} from '../UI/Button';
import useGameHook from '../../hooks/useGameHook';
import {getSession} from '../../hooks/SessionStore';
-const PlayersScreen = () => {
+const PlayersScreen = ({navigation, route}) => {
// Initialisation ------------
const session = getSession();
const {getTeams, getTeamMembers, getGroupMembers, removeMember, resetPlayerProgress, getUser, getAdminWaitlist, approveAdmin, rejectAdmin, getSubgroups} = useGameHook();
const isBusiness = Boolean(session.isBusiness);
+ const isAdmin = Boolean(session.isAcceptedAdmin);
+ const selectedDepartment = route?.params?.department || null;
// State ----------------------
const [loading, setLoading] = useState(true);
const [players, setPlayers] = useState([]);
+ const [departmentList, setDepartmentList] = useState([]);
// Handlers -------------------
const loadData = useCallback(async () => {
if (!session.currentGid) { setLoading(false); return; }
+ const effectiveSGid = isBusiness
+ ? ((isAdmin && selectedDepartment?.SGid) ? selectedDepartment.SGid : (isAdmin ? null : session.currentSGid))
+ : null;
+
+ if (isBusiness && isAdmin && !selectedDepartment) {
+ const sgs = await getSubgroups(session.currentGid);
+ setDepartmentList((sgs || []).filter((sg) => !sg.IsAdminGroup));
+ setPlayers([]);
+ setLoading(false);
+ return;
+ }
+
// Load subgroup names for department display (business accounts)
let subgroupNameMap = {};
if (isBusiness) {
@@ -33,10 +48,10 @@ const PlayersScreen = () => {
}
// Get ALL members (including admins)
- const allMembers = await getGroupMembers(session.currentGid);
+ const allMembers = await getGroupMembers(session.currentGid, effectiveSGid ?? null);
// Get admin waitlist entries
- const waitlist = await getAdminWaitlist(session.currentGid);
+ const waitlist = effectiveSGid ? [] : await getAdminWaitlist(session.currentGid);
// Deduplicate allMembers by Uid — a user may have two rows if they were a
// regular member before being approved as an admin. Prefer the admin row.
@@ -46,7 +61,7 @@ const PlayersScreen = () => {
.filter((m) => { if (seenUids.has(m.Uid)) return false; seenUids.add(m.Uid); return true; });
// Build team lookup: { Uid -> { teamCode, teamName, isLeader } }
- const allTeams = await getTeams(session.currentGid);
+ const allTeams = await getTeams(session.currentGid, effectiveSGid ?? null);
const teamInfoByUid = {};
for (const team of (allTeams || [])) {
const tms = await getTeamMembers(team.Tid);
@@ -107,7 +122,7 @@ const PlayersScreen = () => {
setPlayers(playerList);
setLoading(false);
- }, [session.currentGid]);
+ }, [session.currentGid, session.currentUid, session.currentSGid, isBusiness, isAdmin, selectedDepartment, selectedDepartment?.SGid]);
useEffect(() => { loadData(); }, [loadData]);
@@ -164,8 +179,34 @@ const PlayersScreen = () => {
);
}
+ if (isBusiness && isAdmin && !selectedDepartment) {
+ return (
+
+ Departments
+
+ {departmentList.map((department) => (
+ navigation.navigate('PlayersScreen', {department})}
+ >
+
+
+ {department.SubGroupName}
+ Open
+
+
+
+ ))}
+ {departmentList.length === 0 && (
+ No departments yet.
+ )}
+
+
+ );
+ }
+
return (
-
+
Players
{players.map((player) => (
@@ -255,6 +296,7 @@ const styles = StyleSheet.create({
paddingHorizontal: 15,
paddingTop: 15,
},
+ openLabel: {fontSize: 13, fontWeight: '700', color: '#2563eb'},
listSection: {flex: 1, paddingHorizontal: 15},
emptyText: {color: '#9ca3af', textAlign: 'center', marginTop: 30, fontSize: 14},
playerRow: {
diff --git a/src/components/screens/TeamScreen.js b/src/components/screens/TeamScreen.js
index fe2f52c..d239e62 100644
--- a/src/components/screens/TeamScreen.js
+++ b/src/components/screens/TeamScreen.js
@@ -11,7 +11,8 @@ const TeamScreen = () => {
const session = getSession();
const isAdmin = session.isAcceptedAdmin;
- const {getLobby, 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 ----------------------
@@ -46,11 +47,19 @@ const TeamScreen = () => {
if (!session.currentGid) { setLoading(false); return; }
const group = await getLobby(session.currentGid);
- if (group && !group.TeamsEnabled) {
+ 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);
@@ -70,7 +79,11 @@ const TeamScreen = () => {
if (!teamCode.trim()) return;
// Force uppercase just in case a lowercase code is pasted
const code = teamCode.trim().toUpperCase();
- const result = await joinTeamByCode({JoinCode: code, Uid: session.currentUid});
+ const result = await joinTeamByCode({
+ JoinCode: code,
+ Uid: session.currentUid,
+ ExpectedSGid: session.currentSGid,
+ });
if (!result) return;
// If the server indicates this code matched the admin join code
diff --git a/src/hooks/useGameHook.js b/src/hooks/useGameHook.js
index edd6339..aa6f429 100644
--- a/src/hooks/useGameHook.js
+++ b/src/hooks/useGameHook.js
@@ -89,8 +89,10 @@ const useGameHook = () => {
return response.isSuccess ? response.result : null;
};
- const getGroupMembers = async (gid) => {
- const response = await API.get(`${subgroupMembersEndpoint}?Gid=${gid}`);
+ const getGroupMembers = async (gid, sgid = null) => {
+ let url = `${subgroupMembersEndpoint}?Gid=${gid}`;
+ if (sgid !== null && sgid !== undefined) url += `&SGid=${sgid}`;
+ const response = await API.get(url);
return response.isSuccess ? response.result : [];
};
@@ -100,8 +102,10 @@ const useGameHook = () => {
};
// Teams
- const getTeams = async (gid) => {
- const response = await API.get(`${teamsEndpoint}?Gid=${gid}`);
+ const getTeams = async (gid, sgid = null) => {
+ let url = `${teamsEndpoint}?Gid=${gid}`;
+ if (sgid !== null && sgid !== undefined) url += `&SGid=${sgid}`;
+ const response = await API.get(url);
return response.isSuccess ? response.result : [];
};
@@ -131,7 +135,15 @@ const useGameHook = () => {
};
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;
};