From 4a0468d155045b66f36d63e939e58280c421dd43 Mon Sep 17 00:00:00 2001 From: mudabbir-ahmad Date: Thu, 9 Apr 2026 13:10:59 +0100 Subject: [PATCH] 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. --- src/components/layout/BottomNavbar.js | 18 +++++-- src/components/layout/Screen.js | 2 +- src/components/screens/GameSettingsScreen.js | 55 +++++++++++-------- src/components/screens/LeaderboardScreen.js | 36 ++++++------- src/components/screens/PlayersScreen.js | 56 +++++++++++++++++--- src/components/screens/TeamScreen.js | 19 +++++-- src/hooks/useGameHook.js | 22 ++++++-- 7 files changed, 148 insertions(+), 60 deletions(-) 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; };