Enhance navigation and data handling: update BottomNavbar to pass selected department, refactor GameSettingsScreen and PlayersScreen for improved subgroup management, and adjust API calls to support department-specific data.

This commit is contained in:
bobbert committed 2026-04-09 13:10:59 +01:00
1 parent 43c8bb2bff
commit 4a0468d155
7 files changed
+147 -59

No files matched your search

+15 -3
View File
@@ -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
+1 -1
View File
@@ -35,7 +35,7 @@ const Screen = ({ children, style, showBack = false }) => {
<View style={[styles.screen, isAuthScreen && {paddingTop: insets.top, paddingBottom: insets.bottom}]}>
{!isAuthScreen && <TopNavbar title={pageTitle} showBack={showBack} navigation={navigation} />}
<View style={[styles.content, style]}>{children}</View>
{!isAuthScreen && <BottomNavbar navigation={navigation} routeName={routeName} />}
{!isAuthScreen && <BottomNavbar navigation={navigation} routeName={routeName} routeParams={route.params} />}
<StatusBar style="light" />
</View>
);
+33 -22
View File
@@ -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 (
<Screen>
<ScrollView contentContainerStyle={styles.formContainer} showsVerticalScrollIndicator={false}>
+17 -19
View File
@@ -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]);
+48 -6
View File
@@ -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 (
<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>
<ScrollView style={styles.listSection}>
{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: {
+16 -3
View File
@@ -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
+17 -5
View File
@@ -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;
};