mirror of
https://github.com/mudabbir-ahmad/MAD-Treasure-Hunt.git
synced 2026-10-08 03:00:20 +00:00
327 lines
14 KiB
JavaScript
327 lines
14 KiB
JavaScript
import React, {useCallback, useEffect, useState} from 'react';
|
|
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 = ({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) {
|
|
const sgs = await getSubgroups(session.currentGid);
|
|
for (const sg of (sgs || [])) {
|
|
if (!sg.IsAdminGroup) subgroupNameMap[sg.SGid] = sg.SubGroupName;
|
|
}
|
|
}
|
|
|
|
// Get ALL members (including admins)
|
|
const allMembers = await getGroupMembers(session.currentGid, effectiveSGid ?? null);
|
|
|
|
// Get admin waitlist entries
|
|
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.
|
|
const seenUids = new Set();
|
|
const uniqueMembers = (allMembers || [])
|
|
.sort((a, b) => (b.IsAcceptedAdmin ? 1 : 0) - (a.IsAcceptedAdmin ? 1 : 0))
|
|
.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, effectiveSGid ?? null);
|
|
const teamInfoByUid = {};
|
|
for (const team of (allTeams || [])) {
|
|
const tms = await getTeamMembers(team.Tid);
|
|
for (const tm of (tms || [])) {
|
|
teamInfoByUid[tm.Uid] = {
|
|
teamCode: team.JoinCode,
|
|
teamName: team.TeamName,
|
|
isLeader: Boolean(tm.IsLeader),
|
|
};
|
|
}
|
|
}
|
|
|
|
// Build player list — include admins with tags
|
|
const playerList = [];
|
|
for (const m of uniqueMembers) {
|
|
// Skip the current admin viewing the list
|
|
if (m.Uid === session.currentUid) continue;
|
|
const user = await getUser(m.Uid);
|
|
if (user) {
|
|
// A group member may also be on the admin waitlist (applied after joining as a player).
|
|
// Check the waitlist first so they are treated as pending, not a regular player.
|
|
const waitlistEntry = (waitlist || []).find((w) => w.Uid === m.Uid);
|
|
let adminTag = null;
|
|
let waitlistId = null;
|
|
if (waitlistEntry) {
|
|
adminTag = '[Admin Awaiting Response]';
|
|
waitlistId = waitlistEntry.id;
|
|
} else if (m.IsAcceptedAdmin) {
|
|
adminTag = '[ADMIN]';
|
|
}
|
|
playerList.push({
|
|
...user,
|
|
membershipId: m.id,
|
|
team: teamInfoByUid[m.Uid] || null,
|
|
adminTag,
|
|
waitlistId,
|
|
departmentName: subgroupNameMap[m.SGid] || null,
|
|
});
|
|
}
|
|
}
|
|
|
|
// Include waitlisted users who are not yet group members at all
|
|
for (const w of (waitlist || [])) {
|
|
if (playerList.some((p) => p.Uid === w.Uid)) continue;
|
|
if (w.Uid === session.currentUid) continue;
|
|
const user = await getUser(w.Uid);
|
|
if (user) {
|
|
playerList.push({
|
|
...user,
|
|
membershipId: null,
|
|
team: null,
|
|
adminTag: '[Admin Awaiting Response]',
|
|
waitlistId: w.id,
|
|
departmentName: null,
|
|
});
|
|
}
|
|
}
|
|
|
|
setPlayers(playerList);
|
|
setLoading(false);
|
|
}, [session.currentGid, session.currentUid, session.currentSGid, isBusiness, isAdmin, selectedDepartment, selectedDepartment?.SGid]);
|
|
|
|
useEffect(() => { loadData(); }, [loadData]);
|
|
|
|
const handleRemovePlayer = async (membershipId) => {
|
|
await removeMember(membershipId);
|
|
await loadData();
|
|
};
|
|
|
|
const handleResetPlayerProgress = (player) => {
|
|
Alert.alert(
|
|
'Reset Progress',
|
|
`Reset all cache claims for ${player.username}?`,
|
|
[
|
|
{text: 'Cancel', style: 'cancel'},
|
|
{
|
|
text: 'Reset',
|
|
style: 'destructive',
|
|
onPress: async () => {
|
|
await resetPlayerProgress(session.currentGid, player.Uid);
|
|
Alert.alert('Done', `Progress for ${player.username} has been reset.`);
|
|
},
|
|
},
|
|
],
|
|
);
|
|
};
|
|
|
|
// Approve a waitlisted admin and refresh the list
|
|
const handleApproveAdmin = async (player) => {
|
|
await approveAdmin(player.waitlistId);
|
|
await loadData();
|
|
};
|
|
|
|
// Deny (remove from waitlist) a pending admin and refresh the list
|
|
const handleDenyAdmin = async (player) => {
|
|
await rejectAdmin(player.waitlistId);
|
|
await loadData();
|
|
};
|
|
|
|
// View -----------------------
|
|
|
|
if (loading) {
|
|
return (
|
|
<Screen style={styles.center}>
|
|
<ActivityIndicator size="large"/>
|
|
</Screen>
|
|
);
|
|
}
|
|
|
|
if (!session.currentGid) {
|
|
return (
|
|
<Screen style={styles.center}>
|
|
<Text style={styles.body}>Create or join a game first.</Text>
|
|
</Screen>
|
|
);
|
|
}
|
|
|
|
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) => (
|
|
// Waitlisted admins get a light red tint so they stand out from regular players
|
|
<Card key={player.Uid} style={player.waitlistId ? styles.waitlistCard : undefined}>
|
|
<View style={styles.playerRow}>
|
|
<Text style={styles.playerName} numberOfLines={1}>
|
|
{player.username}
|
|
{player.adminTag && (
|
|
<Text style={player.adminTag === '[ADMIN]' ? styles.adminBadge : styles.waitlistBadge}>
|
|
{' '}{player.adminTag}
|
|
</Text>
|
|
)}
|
|
{/* Show department name in brackets for non-admin players (org accounts) */}
|
|
{!player.adminTag && player.departmentName && (
|
|
<Text style={styles.departmentName}>
|
|
{' '}({player.departmentName})
|
|
</Text>
|
|
)}
|
|
</Text>
|
|
<View style={styles.playerMeta}>
|
|
{player.team?.isLeader && (
|
|
<Text style={styles.leaderBadge}>(Team Leader)</Text>
|
|
)}
|
|
{player.team && (
|
|
<Text style={styles.teamCodeBadge}>{player.team.teamCode}</Text>
|
|
)}
|
|
</View>
|
|
</View>
|
|
<View style={styles.actionRow}>
|
|
{/* Waitlisted admins get Approve / Deny instead of the normal player actions */}
|
|
{player.waitlistId ? (
|
|
<>
|
|
<Button
|
|
label="Approve"
|
|
onClick={() => handleApproveAdmin(player)}
|
|
styleButton={styles.approveButton}
|
|
styleLabel={styles.actionLabel}
|
|
/>
|
|
<Button
|
|
label="Deny"
|
|
onClick={() => handleDenyAdmin(player)}
|
|
styleButton={styles.denyButton}
|
|
styleLabel={styles.actionLabel}
|
|
/>
|
|
</>
|
|
) : (
|
|
<>
|
|
{!player.adminTag && (
|
|
<Button
|
|
label="Reset Progress"
|
|
onClick={() => handleResetPlayerProgress(player)}
|
|
styleButton={styles.resetButton}
|
|
styleLabel={styles.actionLabel}
|
|
/>
|
|
)}
|
|
{player.membershipId && (
|
|
<Button
|
|
label="Kick"
|
|
onClick={() => handleRemovePlayer(player.membershipId)}
|
|
styleButton={styles.removeButton}
|
|
styleLabel={styles.actionLabel}
|
|
/>
|
|
)}
|
|
</>
|
|
)}
|
|
</View>
|
|
</Card>
|
|
))}
|
|
{players.length === 0 && (
|
|
<Text style={styles.emptyText}>No players have joined yet.</Text>
|
|
)}
|
|
</ScrollView>
|
|
</Screen>
|
|
);
|
|
};
|
|
|
|
const styles = StyleSheet.create({
|
|
center: {justifyContent: 'center', alignItems: 'center', flex: 1},
|
|
container: {padding: 0},
|
|
body: {color: '#4b5563', fontSize: 15},
|
|
sectionTitle: {
|
|
fontSize: 20,
|
|
fontWeight: '700',
|
|
color: '#1f2937',
|
|
marginBottom: 12,
|
|
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: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: 8,
|
|
marginBottom: 8,
|
|
},
|
|
playerName: {fontSize: 15, fontWeight: '600', color: '#1f2937', flex: 1},
|
|
playerMeta: {flexDirection: 'row', alignItems: 'center', gap: 6},
|
|
leaderBadge: {fontSize: 11, color: '#2563eb', fontWeight: '700'},
|
|
teamCodeBadge: {fontSize: 12, fontWeight: '700', color: '#16a34a', letterSpacing: 1},
|
|
adminBadge: {fontSize: 12, fontWeight: '700', color: '#dc2626'},
|
|
waitlistBadge: {fontSize: 11, fontWeight: '600', color: '#f59e0b'},
|
|
actionRow: {flexDirection: 'row', gap: 8},
|
|
resetButton: {backgroundColor: '#f59e0b', borderColor: '#f59e0b', minHeight: 36, flex: 1, paddingHorizontal: 10},
|
|
removeButton: {backgroundColor: '#dc2626', borderColor: '#dc2626', minHeight: 36, flex: 1, paddingHorizontal: 10},
|
|
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
|
|
waitlistCard: {backgroundColor: '#FFD1DC', borderColor: '#fca5a5'},
|
|
// Approve / deny buttons for waitlisted admins
|
|
approveButton: {backgroundColor: '#16a34a', borderColor: '#16a34a', minHeight: 36, flex: 1, paddingHorizontal: 10},
|
|
denyButton: {backgroundColor: '#dc2626', borderColor: '#dc2626', minHeight: 36, flex: 1, paddingHorizontal: 10},
|
|
});
|
|
|
|
export default PlayersScreen;
|