Enhance department management: add department name display in cache cards, update navigation to include selected department, and improve department selection in map screen.

This commit is contained in:
bobbert committed 2026-04-09 12:49:05 +01:00
1 parent dd5dd83737
commit 43c8bb2bff
5 files changed
+121 -9

No files matched your search

+5 -1
View File
@@ -2,7 +2,7 @@ import {StyleSheet, Text, View} from 'react-native';
import Card from '../UI/Card';
import {Button} from '../UI/Button';
const CacheCardItem = ({cache, isAdmin, isClaimed, isSelected, onEdit, onDelete, onSelect, disabled}) => {
const CacheCardItem = ({cache, isAdmin, isClaimed, isSelected, onEdit, onDelete, onSelect, disabled, departmentName}) => {
// Initialisation -------------
const displayName = cache.name || cache.clue || 'Unnamed Cache';
@@ -16,6 +16,9 @@ const CacheCardItem = ({cache, isAdmin, isClaimed, isSelected, onEdit, onDelete,
<View style={styles.topRow}>
<View style={styles.nameWrap}>
<Text style={[styles.name, isClaimed && styles.claimedText, disabled && styles.disabledText]} numberOfLines={1}>{displayName}</Text>
{isAdmin && departmentName ? (
<Text style={styles.departmentText} numberOfLines={1}>Department: {departmentName}</Text>
) : null}
{cache.name ? (
<Text style={styles.clue} numberOfLines={1}>{cache.clue}</Text>
) : null}
@@ -87,6 +90,7 @@ const styles = StyleSheet.create({
marginBottom: 8,
},
clue: {fontSize: 13, color: '#6b7280', marginTop: 2},
departmentText: {fontSize: 13, color: '#2563eb', marginTop: 2, fontWeight: '600'},
nameWrap: {flex: 1, marginRight: 8},
name: {fontSize: 15, fontWeight: '600', color: '#1f2937'},
claimedText: {color: '#9ca3af'},
@@ -69,7 +69,10 @@ const DepartmentSettingsScreen = ({route, navigation}) => {
};
const handleManageCaches = () => {
navigation.navigate('MapScreen');
navigation.navigate('MapScreen', {
selectedDepartmentSGid: SGid,
selectedDepartmentName: deptName || null,
});
};
// View -----------------------
+1 -1
View File
@@ -254,7 +254,7 @@ const GameSettingsScreen = ({navigation}) => {
</View>
<View style={styles.deptCardActions}>
<Button
label="Open Department Game Settings"
label="Department Game"
onClick={() => handleEnterDepartment(sg)}
styleButton={styles.deptOpenButton}
styleLabel={styles.deptBtnLabel}
+100 -4
View File
@@ -16,7 +16,7 @@ import {getFovCone} from '../../utils/geoMath';
const DEFAULT_REGION = {latitude: 51.5074, longitude: -0.1278, latitudeDelta: 0.05, longitudeDelta: 0.05};
const MapScreen = ({navigation}) => {
const MapScreen = ({navigation, route}) => {
// Initialisation ------------
const session = getSession();
@@ -25,6 +25,7 @@ const MapScreen = ({navigation}) => {
// Stable ref to prevent infinite re-render loops
const getCachesRef = useRef(getCaches);
getCachesRef.current = getCaches;
const lastAppliedRouteDepartmentRef = useRef(null);
// State ----------------------
@@ -54,6 +55,8 @@ const MapScreen = ({navigation}) => {
const [newCoord, setNewCoord] = useState(null);
const [newName, setNewName] = useState('');
const [newClue, setNewClue] = useState('');
const [selectedCacheSubgroupId, setSelectedCacheSubgroupId] = useState(null);
const [departmentDropdownOpen, setDepartmentDropdownOpen] = useState(false);
const claimDistance = groupInfo?.CacheTriggerMeters || 20;
const isPlayer = inGame && !isAdmin;
@@ -93,7 +96,36 @@ const MapScreen = ({navigation}) => {
}, [inGame]);
// Default member subgroup SGid for cache creation (first non-admin subgroup)
const defaultMemberSGid = subgroups.find((sg) => !sg.IsAdminGroup)?.SGid || null;
const departments = useMemo(
() => subgroups.filter((sg) => !sg.IsAdminGroup),
[subgroups],
);
const defaultMemberSGid = departments[0]?.SGid || null;
const departmentNamesById = useMemo(() => {
const map = {};
for (const subgroup of departments) {
map[String(subgroup.SGid)] = subgroup.SubGroupName || `Department ${subgroup.SGid}`;
}
return map;
}, [departments]);
useEffect(() => {
if (!isAdmin || departments.length === 0) return;
const routeSelectedSgid = route?.params?.selectedDepartmentSGid;
const hasRouteDepartment = departments.some((sg) => String(sg.SGid) === String(routeSelectedSgid));
const shouldApplyRouteDepartment =
hasRouteDepartment && String(lastAppliedRouteDepartmentRef.current) !== String(routeSelectedSgid);
if (shouldApplyRouteDepartment) {
setSelectedCacheSubgroupId(routeSelectedSgid);
lastAppliedRouteDepartmentRef.current = routeSelectedSgid;
return;
}
const currentStillValid = departments.some((sg) => String(sg.SGid) === String(selectedCacheSubgroupId));
if (!currentStillValid) {
setSelectedCacheSubgroupId(defaultMemberSGid);
}
}, [route?.params?.selectedDepartmentSGid, departments, isAdmin, selectedCacheSubgroupId, defaultMemberSGid]);
const loadCaches = useCallback(async () => {
const currentGid = getSession().currentGid;
@@ -234,7 +266,11 @@ const MapScreen = ({navigation}) => {
const handleJoinDepartment = async () => {
setJoinError('');
if (!deptCode.trim()) return;
const result = await joinPrivateGame({JoinCode: deptCode.trim().toUpperCase(), Uid: session.currentUid});
const result = await joinPrivateGame({
JoinCode: deptCode.trim().toUpperCase(),
Uid: session.currentUid,
ExpectedGid: matchedGroup.Gid,
});
if (!result) {
setJoinError('Invalid department code. Check the code and try again.');
return;
@@ -273,6 +309,8 @@ const MapScreen = ({navigation}) => {
setNewCoord(fallback);
setNewName('');
setNewClue('');
if (!selectedCacheSubgroupId && defaultMemberSGid) setSelectedCacheSubgroupId(defaultMemberSGid);
setDepartmentDropdownOpen(false);
setIsCreating(true);
};
@@ -281,6 +319,8 @@ const MapScreen = ({navigation}) => {
setNewCoord(cache.coordinates);
setNewName(cache.name || '');
setNewClue(cache.clue || '');
setSelectedCacheSubgroupId(cache.subgroupId || defaultMemberSGid);
setDepartmentDropdownOpen(false);
setIsCreating(true);
};
@@ -292,24 +332,28 @@ const MapScreen = ({navigation}) => {
const handleSaveCache = async () => {
if (!newCoord || !newClue.trim() || !session.currentGid) return;
const subgroupId = selectedCacheSubgroupId || defaultMemberSGid;
if (!subgroupId) return;
const payload = {
gid: session.currentGid,
name: newName.trim(),
latitude: newCoord.latitude,
longitude: newCoord.longitude,
clue: newClue.trim(),
subgroupId: defaultMemberSGid,
subgroupId,
};
if (editingCacheId) payload.cacheId = editingCacheId;
await upsertCache(payload);
setIsCreating(false);
setEditingCacheId(null);
setDepartmentDropdownOpen(false);
await loadCaches();
};
const handleCancelCreate = () => {
setIsCreating(false);
setEditingCacheId(null);
setDepartmentDropdownOpen(false);
};
const handleExpandMap = () => {
@@ -491,6 +535,32 @@ const MapScreen = ({navigation}) => {
</MapView>
</View>
<ScrollView style={styles.formSection}>
<Text style={styles.departmentLabel}>Department</Text>
<Pressable
style={styles.departmentSelector}
onPress={() => setDepartmentDropdownOpen((prev) => !prev)}
>
<Text style={styles.departmentSelectorText}>
{departmentNamesById[String(selectedCacheSubgroupId)] || 'Select department'}
</Text>
<Text style={styles.departmentSelectorChevron}>{departmentDropdownOpen ? '▲' : '▼'}</Text>
</Pressable>
{departmentDropdownOpen && (
<View style={styles.departmentMenu}>
{departments.map((department) => (
<Pressable
key={department.SGid}
style={styles.departmentOption}
onPress={() => {
setSelectedCacheSubgroupId(department.SGid);
setDepartmentDropdownOpen(false);
}}
>
<Text style={styles.departmentOptionText}>{department.SubGroupName}</Text>
</Pressable>
))}
</View>
)}
<TextInput
style={styles.formInput}
placeholder="Cache Name"
@@ -598,6 +668,7 @@ const MapScreen = ({navigation}) => {
key={cache.id}
cache={cache}
isAdmin={true}
departmentName={departmentNamesById[String(cache.subgroupId)] || 'Unassigned'}
onEdit={handleEditCache}
onDelete={handleDeleteCache}
/>
@@ -754,6 +825,31 @@ const styles = StyleSheet.create({
backgroundColor: '#ffffff',
marginBottom: 10,
},
departmentLabel: {fontSize: 13, fontWeight: '600', color: '#374151', marginBottom: 6},
departmentSelector: {
borderWidth: 1,
borderColor: '#d1d5db',
borderRadius: 8,
backgroundColor: '#ffffff',
paddingHorizontal: 12,
paddingVertical: 10,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 8,
},
departmentSelectorText: {color: '#1f2937', fontSize: 15, fontWeight: '500'},
departmentSelectorChevron: {color: '#6b7280', fontSize: 12, fontWeight: '700'},
departmentMenu: {
borderWidth: 1,
borderColor: '#e5e7eb',
borderRadius: 8,
backgroundColor: '#ffffff',
marginBottom: 10,
overflow: 'hidden',
},
departmentOption: {paddingHorizontal: 12, paddingVertical: 10, borderBottomWidth: 1, borderBottomColor: '#f3f4f6'},
departmentOptionText: {color: '#374151', fontSize: 14},
coordRow: {flexDirection: 'row', gap: 10},
coordField: {flex: 1},
coordLabel: {fontSize: 12, fontWeight: '600', color: '#6b7280', marginBottom: 4},
+11 -2
View File
@@ -43,7 +43,9 @@ const useGameHook = () => {
// Look up a group by its Organisation Join Code
const getGroupByOrgCode = async (code) => {
const response = await API.get(`${groupsEndpoint}?OrgJoinCode=${code}`);
const normalizedCode = String(code || '').trim().toUpperCase();
if (!normalizedCode) return null;
const response = await API.get(`${groupsEndpoint}?OrgJoinCode=${encodeURIComponent(normalizedCode)}`);
if (response.isSuccess && response.result.length > 0) return response.result[0];
return null;
};
@@ -76,7 +78,14 @@ const useGameHook = () => {
// Subgroup members
const joinPrivateGame = async (payload) => {
const response = await API.post(subgroupMembersEndpoint, payload);
const data = {
JoinCode: payload.JoinCode,
Uid: payload.Uid,
};
if (payload.ExpectedGid !== undefined && payload.ExpectedGid !== null) {
data.ExpectedGid = payload.ExpectedGid;
}
const response = await API.post(subgroupMembersEndpoint, data);
return response.isSuccess ? response.result : null;
};