Refactor business settings: update department-related UI elements, enhance game reset functionality, and improve data handling for business accounts.

This commit is contained in:
bobbert committed 2026-04-09 12:32:29 +01:00
1 parent e0065e885f
commit dd5dd83737
6 files changed
+130 -74

No files matched your search

@@ -60,7 +60,7 @@ const DepartmentSettingsScreen = ({route, navigation}) => {
text: 'Reset', text: 'Reset',
style: 'destructive', style: 'destructive',
onPress: async () => { onPress: async () => {
await resetGame(Gid); await resetGame(Gid, SGid);
Alert.alert('Done', 'Department progress has been reset.'); Alert.alert('Done', 'Department progress has been reset.');
}, },
}, },
+34 -24
View File
@@ -10,7 +10,7 @@ const GameSettingsScreen = ({navigation}) => {
// Initialisation ------------ // Initialisation ------------
const session = getSession(); const session = getSession();
const {getLobby, updateGroup, getSubgroups, createSubgroup, updateSubgroup, deleteSubgroup, resetGame, getAdminWaitlist, approveAdmin, rejectAdmin, getUser, disbandTeams} = useGameHook(); const {getLobby, updateGroup, getSubgroups, createSubgroup, deleteSubgroup, resetGame, getAdminWaitlist, approveAdmin, rejectAdmin, getUser, disbandTeams} = useGameHook();
const isBusiness = Boolean(session.isBusiness); const isBusiness = Boolean(session.isBusiness);
// State ---------------------- // State ----------------------
@@ -209,24 +209,24 @@ const GameSettingsScreen = ({navigation}) => {
return ( return (
<Screen> <Screen>
<ScrollView contentContainerStyle={styles.formContainer} showsVerticalScrollIndicator={false}> <ScrollView contentContainerStyle={styles.formContainer} showsVerticalScrollIndicator={false}>
<Text style={styles.sectionTitle}>Business Settings</Text> <Text style={styles.sectionTitle}>Game Configuration :</Text>
<Text style={styles.label}>Business Name</Text> <Text style={styles.label}>Organisation Name:</Text>
<TextInput <TextInput
style={styles.input} style={styles.input}
value={businessName} value={businessName}
onChangeText={setBusinessName} onChangeText={setBusinessName}
placeholder="Enter business name" placeholder="Enter organisation name"
placeholderTextColor="#9ca3af" placeholderTextColor="#9ca3af"
/> />
<View style={styles.codeSection}> <View style={styles.codeSection}>
<Text style={styles.codeLabel}>Admin Join Code</Text> <Text style={styles.codeLabel}>Admin join code:</Text>
<Text style={styles.codeValue}>{adminJoinCode || '—'}</Text> <Text style={styles.codeValue}>{adminJoinCode || '—'}</Text>
</View> </View>
<View style={styles.codeSection}> <View style={styles.codeSection}>
<Text style={styles.codeLabel}>Organisation Join Code</Text> <Text style={styles.codeLabel}>Org join code:</Text>
<Text style={styles.codeValue}>{orgJoinCode || '—'}</Text> <Text style={styles.codeValue}>{orgJoinCode || '—'}</Text>
</View> </View>
@@ -243,27 +243,36 @@ const GameSettingsScreen = ({navigation}) => {
{/* Departments List */} {/* Departments List */}
<View style={styles.deptSection}> <View style={styles.deptSection}>
<Text style={styles.sectionTitle}>Departments</Text> <Text style={styles.sectionTitle}>Departments:</Text>
{departments.map((sg) => ( {departments.map((sg, index) => (
<Card key={sg.SGid}> <Card key={sg.SGid}>
<View style={styles.deptRow}> <Text style={styles.deptIndexLabel}>Department {index + 1}:</Text>
<View style={styles.deptInfo}> <View style={styles.deptCardRow}>
<View style={styles.deptCardInfo}>
<Text style={styles.deptInfoLabel}>Department Name</Text>
<Text style={styles.deptName}>{sg.SubGroupName}</Text> <Text style={styles.deptName}>{sg.SubGroupName}</Text>
<Text style={styles.deptCode}>Join Code: {sg.JoinCode || '—'}</Text>
</View> </View>
<View style={styles.deptActions}> <View style={styles.deptCardActions}>
<Button <Button
label="Delete" label="Open Department Game Settings"
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)} onClick={() => handleDeleteDepartment(sg)}
styleButton={styles.deptDeleteButton} styleButton={styles.deptDeleteButton}
styleLabel={styles.deptBtnLabel} styleLabel={styles.deptBtnLabel}
/> />
<Button
label="→"
onClick={() => handleEnterDepartment(sg)}
styleButton={styles.deptArrowButton}
styleLabel={styles.deptArrowLabel}
/>
</View> </View>
</View> </View>
</Card> </Card>
@@ -475,15 +484,16 @@ const styles = StyleSheet.create({
saveLabel: {color: '#ffffff', fontWeight: '600'}, saveLabel: {color: '#ffffff', fontWeight: '600'},
// Department / subgroup styles // Department / subgroup styles
deptSection: {marginTop: 30}, deptSection: {marginTop: 30},
deptRow: {flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center'}, deptIndexLabel: {fontSize: 14, fontWeight: '700', color: '#374151', marginBottom: 8},
deptInfo: {flex: 1}, deptCardRow: {flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8},
deptCardInfo: {flex: 1, paddingRight: 10},
deptCardActions: {flexDirection: 'row', gap: 6, flex: 0},
deptInfoLabel: {fontSize: 12, fontWeight: '600', color: '#6b7280', marginBottom: 2},
deptName: {fontSize: 15, fontWeight: '700', color: '#1f2937'}, deptName: {fontSize: 15, fontWeight: '700', color: '#1f2937'},
deptCode: {fontSize: 13, fontWeight: '600', color: '#2563eb', marginTop: 2, letterSpacing: 1}, deptCode: {fontSize: 13, fontWeight: '600', color: '#2563eb', marginTop: 2, letterSpacing: 1},
deptActions: {flexDirection: 'row', gap: 6},
deptDeleteButton: {backgroundColor: '#dc2626', borderColor: '#dc2626', minHeight: 36, flex: 0, paddingHorizontal: 10}, deptDeleteButton: {backgroundColor: '#dc2626', borderColor: '#dc2626', minHeight: 36, flex: 0, paddingHorizontal: 10},
deptOpenButton: {backgroundColor: '#2563eb', borderColor: '#2563eb', minHeight: 36, flex: 0, paddingHorizontal: 10},
deptBtnLabel: {color: '#ffffff', fontWeight: '600', fontSize: 13}, deptBtnLabel: {color: '#ffffff', fontWeight: '600', fontSize: 13},
deptArrowButton: {backgroundColor: '#2563eb', borderColor: '#2563eb', minHeight: 36, flex: 0, paddingHorizontal: 14},
deptArrowLabel: {color: '#ffffff', fontWeight: '700', fontSize: 18},
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: '#16a34a', borderColor: '#16a34a', flex: 0, paddingHorizontal: 16},
createDeptLabel: {color: '#ffffff', fontWeight: '600'}, createDeptLabel: {color: '#ffffff', fontWeight: '600'},
+63 -8
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, resetTeamProgress} = useGameHook(); const isBusiness = Boolean(session.isBusiness);
const selectedDepartment = route?.params?.department || null;
const {getLobby, 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 -------------------
@@ -33,7 +36,18 @@ const LeaderboardScreen = () => {
const isTeams = Boolean(group?.TeamsEnabled); const isTeams = Boolean(group?.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 leaderboardSGid = (isBusiness && isAdmin && selectedDepartment?.SGid)
? selectedDepartment.SGid
: null;
const caches = await getCaches(session.currentGid, leaderboardSGid);
const total = (caches || []).length; const total = (caches || []).length;
setTotalCacheCount(total); setTotalCacheCount(total);
@@ -42,7 +56,11 @@ const LeaderboardScreen = () => {
const teams = await getTeams(session.currentGid); const teams = await getTeams(session.currentGid);
const teamData = []; const teamData = [];
for (const team of (teams || [])) { const scopedTeams = leaderboardSGid
? (teams || []).filter((team) => !team.SGid || team.SGid === leaderboardSGid)
: (teams || []);
for (const team of scopedTeams) {
const members = await getTeamMembers(team.Tid); const members = await getTeamMembers(team.Tid);
const memberData = []; const memberData = [];
for (const m of (members || [])) { for (const m of (members || [])) {
@@ -81,8 +99,11 @@ const LeaderboardScreen = () => {
} else { } else {
// Build player rankings (no teams) // Build player rankings (no teams)
const allMembers = await getGroupMembers(session.currentGid); const allMembers = await getGroupMembers(session.currentGid);
const scopedMembers = leaderboardSGid
? (allMembers || []).filter((mem) => mem.SGid === leaderboardSGid)
: (allMembers || []);
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 +119,7 @@ const LeaderboardScreen = () => {
} }
setLoading(false); setLoading(false);
}, [session.currentGid, session.currentTid]); }, [session.currentGid, session.currentTid, isBusiness, isAdmin, selectedDepartment, selectedDepartment?.SGid]);
useEffect(() => { loadData(); }, [loadData]); useEffect(() => { loadData(); }, [loadData]);
@@ -166,6 +187,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}>
@@ -269,7 +316,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 && (
@@ -282,7 +332,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 && (
@@ -361,6 +414,8 @@ 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: '#4b5563', fontSize: 15},
departmentTitle: {fontSize: 20, fontWeight: '700', color: '#1f2937', paddingHorizontal: 15, paddingTop: 15, marginBottom: 6},
openLabel: {fontSize: 13, fontWeight: '700', color: '#2563eb'},
tabRow: { tabRow: {
flexDirection: 'row', flexDirection: 'row',
borderBottomWidth: 1, borderBottomWidth: 1,
+7 -37
View File
@@ -42,7 +42,7 @@ const MapScreen = ({navigation}) => {
const [subgroups, setSubgroups] = useState([]); const [subgroups, setSubgroups] = useState([]);
// Business join flow state // Business join flow state
const [joinStep, setJoinStep] = useState(null); // null | 'orgCode' | 'deptCode' const [joinStep, setJoinStep] = useState(null); // null | 'deptCode'
const [orgCode, setOrgCode] = useState(''); const [orgCode, setOrgCode] = useState('');
const [deptCode, setDeptCode] = useState(''); const [deptCode, setDeptCode] = useState('');
const [matchedGroup, setMatchedGroup] = useState(null); const [matchedGroup, setMatchedGroup] = useState(null);
@@ -365,7 +365,7 @@ const MapScreen = ({navigation}) => {
<ButtonTray> <ButtonTray>
<Button <Button
label="Back" label="Back"
onClick={() => { setJoinStep('orgCode'); setDeptCode(''); setJoinError(''); }} onClick={() => { setJoinStep(null); setDeptCode(''); setJoinError(''); }}
styleButton={styles.backButton} styleButton={styles.backButton}
styleLabel={styles.backLabel} styleLabel={styles.backLabel}
/> />
@@ -375,48 +375,28 @@ const MapScreen = ({navigation}) => {
); );
} }
// Step 1: Enter org code (or choose create / join) // Default: Join company or Create company choice
if (joinStep === 'orgCode') {
return ( return (
<Screen style={styles.center}> <Screen style={styles.center}>
<Text style={styles.bizTitle}>Join a Company</Text> <Text style={styles.bizTitle}>Welcome</Text>
<Text style={styles.bizSubtitle}>Enter the organisation code provided by your company</Text> <Text style={styles.bizSubtitle}>Set up your organisation or join an existing one</Text>
<View style={styles.inputRow}> <View style={styles.inputRow}>
<TextInput <TextInput
style={styles.codeInput} style={styles.codeInput}
placeholder="Organisation Code" placeholder="Company Code"
placeholderTextColor="#9ca3af" placeholderTextColor="#9ca3af"
value={orgCode} value={orgCode}
onChangeText={(text) => setOrgCode(text.toUpperCase())} onChangeText={(text) => setOrgCode(text.toUpperCase())}
autoCapitalize="characters" autoCapitalize="characters"
/> />
<Button <Button
label="Next" label="Join Company"
onClick={handleVerifyOrgCode} onClick={handleVerifyOrgCode}
styleButton={styles.joinButton} styleButton={styles.joinButton}
styleLabel={styles.joinLabel} styleLabel={styles.joinLabel}
/> />
</View> </View>
{joinError ? <Text style={styles.joinError}>{joinError}</Text> : null} {joinError ? <Text style={styles.joinError}>{joinError}</Text> : null}
<View style={styles.fullRow}>
<ButtonTray>
<Button
label="Back"
onClick={() => { setJoinStep(null); setOrgCode(''); setJoinError(''); }}
styleButton={styles.backButton}
styleLabel={styles.backLabel}
/>
</ButtonTray>
</View>
</Screen>
);
}
// Default: Create company or Join 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.fullRow}> <View style={styles.fullRow}>
<ButtonTray> <ButtonTray>
<Button <Button
@@ -427,16 +407,6 @@ const MapScreen = ({navigation}) => {
/> />
</ButtonTray> </ButtonTray>
</View> </View>
<View style={styles.fullRow}>
<ButtonTray>
<Button
label="Join Company"
onClick={() => setJoinStep('orgCode')}
styleButton={styles.joinButton}
styleLabel={styles.joinLabel}
/>
</ButtonTray>
</View>
</Screen> </Screen>
); );
} }
+20 -1
View File
@@ -10,7 +10,8 @@ const PlayersScreen = () => {
// 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);
// State ---------------------- // State ----------------------
@@ -22,6 +23,15 @@ const PlayersScreen = () => {
const loadData = useCallback(async () => { const loadData = useCallback(async () => {
if (!session.currentGid) { setLoading(false); return; } if (!session.currentGid) { 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);
@@ -73,6 +83,7 @@ const PlayersScreen = () => {
team: teamInfoByUid[m.Uid] || null, team: teamInfoByUid[m.Uid] || null,
adminTag, adminTag,
waitlistId, waitlistId,
departmentName: subgroupNameMap[m.SGid] || null,
}); });
} }
} }
@@ -89,6 +100,7 @@ const PlayersScreen = () => {
team: null, team: null,
adminTag: '[Admin Awaiting Response]', adminTag: '[Admin Awaiting Response]',
waitlistId: w.id, waitlistId: w.id,
departmentName: null,
}); });
} }
} }
@@ -167,6 +179,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 && (
@@ -255,6 +273,7 @@ const styles = StyleSheet.create({
resetButton: {backgroundColor: '#f59e0b', borderColor: '#f59e0b', minHeight: 36, flex: 1, paddingHorizontal: 10}, resetButton: {backgroundColor: '#f59e0b', borderColor: '#f59e0b', minHeight: 36, flex: 1, paddingHorizontal: 10},
removeButton: {backgroundColor: '#dc2626', borderColor: '#dc2626', minHeight: 36, flex: 1, paddingHorizontal: 10}, removeButton: {backgroundColor: '#dc2626', borderColor: '#dc2626', minHeight: 36, flex: 1, paddingHorizontal: 10},
actionLabel: {color: '#ffffff', fontWeight: '600', fontSize: 13}, actionLabel: {color: '#ffffff', fontWeight: '600', fontSize: 13},
departmentName: {fontSize: 14, color: '#6b7280', fontStyle: 'italic'},
// Waitlisted admin card tint — light red so they read as pending, not regular players // Waitlisted admin card tint — light red so they read as pending, not regular players
waitlistCard: {backgroundColor: '#FFD1DC', borderColor: '#fca5a5'}, waitlistCard: {backgroundColor: '#FFD1DC', borderColor: '#fca5a5'},
// Approve / deny buttons for waitlisted admins // Approve / deny buttons for waitlisted admins
+5 -3
View File
@@ -175,9 +175,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;
}; };