diff --git a/backend/src/routes/users.js b/backend/src/routes/users.js index 5abf4be..4efd468 100644 --- a/backend/src/routes/users.js +++ b/backend/src/routes/users.js @@ -80,18 +80,18 @@ router.get('/search', authMiddleware, async (req, res) => { const group = await queryOne(req.schema, 'SELECT type, is_direct FROM groups WHERE id = $1', [parseInt(groupId)]); if (group && (group.type === 'private' || group.is_direct)) { users = await query(req.schema, - `SELECT u.id,u.name,u.display_name,u.avatar,u.role,u.status,u.hide_admin_tag,u.allow_dm,u.is_minor FROM users u JOIN group_members gm ON gm.user_id=u.id AND gm.group_id=$1 WHERE u.status='active' AND u.id!=$2 AND (u.name ILIKE $3 OR u.display_name ILIKE $3) ORDER BY u.name ASC${isTyped ? ' LIMIT 10' : ''}`, + `SELECT u.id,u.name,u.display_name,u.avatar,u.role,u.status,u.hide_admin_tag,u.allow_dm,u.is_minor,u.is_default_admin FROM users u JOIN group_members gm ON gm.user_id=u.id AND gm.group_id=$1 WHERE u.status='active' AND u.id!=$2 AND (u.name ILIKE $3 OR u.display_name ILIKE $3) ORDER BY u.name ASC${isTyped ? ' LIMIT 10' : ''}`, [parseInt(groupId), req.user.id, `%${q}%`] ); } else { users = await query(req.schema, - `SELECT id,name,display_name,avatar,role,status,hide_admin_tag,allow_dm,is_minor FROM users WHERE status='active' AND id!=$1 AND (name ILIKE $2 OR display_name ILIKE $2) ORDER BY name ASC${isTyped ? ' LIMIT 10' : ''}`, + `SELECT id,name,display_name,avatar,role,status,hide_admin_tag,allow_dm,is_minor,is_default_admin FROM users WHERE status='active' AND id!=$1 AND (name ILIKE $2 OR display_name ILIKE $2) ORDER BY name ASC${isTyped ? ' LIMIT 10' : ''}`, [req.user.id, `%${q}%`] ); } } else { users = await query(req.schema, - `SELECT id,name,display_name,avatar,role,status,hide_admin_tag,allow_dm,is_minor FROM users WHERE status='active' AND (name ILIKE $1 OR display_name ILIKE $1) ORDER BY name ASC${isTyped ? ' LIMIT 10' : ''}`, + `SELECT id,name,display_name,avatar,role,status,hide_admin_tag,allow_dm,is_minor,is_default_admin FROM users WHERE status='active' AND (name ILIKE $1 OR display_name ILIKE $1) ORDER BY name ASC${isTyped ? ' LIMIT 10' : ''}`, [`%${q}%`] ); } diff --git a/frontend/src/components/AddChildAliasModal.jsx b/frontend/src/components/AddChildAliasModal.jsx index cb0402e..2fe6007 100644 --- a/frontend/src/components/AddChildAliasModal.jsx +++ b/frontend/src/components/AddChildAliasModal.jsx @@ -40,7 +40,7 @@ export default function AddChildAliasModal({ features = {}, onClose }) { setPartner(p); setSelectedPartnerId(p?.id?.toString() || ''); setRespondSeparately(p?.respond_separately || false); - setAllUsers((usersRes.users || []).filter(u => u.id !== currentUser?.id)); + setAllUsers((usersRes.users || []).filter(u => u.id !== currentUser?.id && !u.is_default_admin)); if (isMixedAge) { setMinorPlayers(thirdRes.users || []); } else { diff --git a/frontend/src/components/SettingsModal.jsx b/frontend/src/components/SettingsModal.jsx index fdc66fe..19bdf0e 100644 --- a/frontend/src/components/SettingsModal.jsx +++ b/frontend/src/components/SettingsModal.jsx @@ -69,7 +69,7 @@ function MessagesTab() { const rows = [ { key: 'msgPublic', label: 'Public Messages', desc: 'Public group channels visible to all members.' }, - { key: 'msgGroup', label: 'Group Messages', desc: 'Private group messages managed by User Groups.' }, + { key: 'msgGroup', label: 'User Group Messages', desc: 'Private group messages managed by User Groups.' }, { key: 'msgPrivateGroup', label: 'Private Group Messages', desc: 'Private multi-member group conversations.' }, { key: 'msgU2U', label: 'Private Messages (U2U)', desc: 'One-on-one direct messages between users.' }, ]; @@ -162,18 +162,18 @@ function TeamManagementTab() { const LOGIN_TYPE_OPTIONS = [ { id: 'all_ages', - label: 'Unrestricted', - desc: 'No age restrictions. All users interact normally. Default behaviour.', + label: 'Unrestricted (default)', + desc: 'No age restrictions. All users interact normally.', }, { id: 'guardian_only', label: 'Guardian Only', - desc: "Parents are required to add their child's details in their profile. They respond on behalf of the child for events with availability tracking for the players group.", + desc: "Parents/Guardians login one. Parents/Guardians are required to add their child's details in the \"Family Manager\". They will also respond on behalf of the child for events with availability tracking.", }, { id: 'mixed_age', label: 'Restricted', - desc: "Parents, or user managers, add the minor's user account to their guardian profile. Minor aged users cannot login until a manager approves the guardian link.", + desc: "No age restriction for login. Date of Birth is a required field. Parents/Guardians must select their child in the Family Manager to allow them to login. Any private message initiated by any adult to a minor aged user will include the child's designated guardian.", }, ]; @@ -247,7 +247,7 @@ function LoginTypeTab() {
-

The user group that children / aliases are added to.

+

Select a group that minor aged users will be put in by default. *

setGuardiansGroupId(e.target.value)}> {userGroups.map(g => )}
+

+ * Open Group Manager to create a different group, if none are suitable in these lists. +

)} @@ -396,7 +399,7 @@ function RegistrationTab({ onFeaturesChanged }) { // ── Main modal ──────────────────────────────────────────────────────────────── export default function SettingsModal({ onClose, onFeaturesChanged }) { - const [tab, setTab] = useState('messages'); + const [tab, setTab] = useState('login-type'); const [appType, setAppType] = useState('RosterChirp-Chat'); useEffect(() => { @@ -424,9 +427,9 @@ export default function SettingsModal({ onClose, onFeaturesChanged }) {
diff --git a/frontend/src/pages/Chat.jsx b/frontend/src/pages/Chat.jsx index 5804a03..20705f2 100644 --- a/frontend/src/pages/Chat.jsx +++ b/frontend/src/pages/Chat.jsx @@ -137,15 +137,18 @@ export default function Chat() { }).catch(() => {}); }, [features.loginType, features.inGuardiansGroup]); - // Close help — open deferred add-child popup if pending + // Close help — open deferred add-child popup if pending, or settings for first-time default admin const handleHelpClose = useCallback(() => { if (addChildPending) { setAddChildPending(false); setModal('addchild'); + } else if (!helpDismissed && user?.is_default_admin && !localStorage.getItem('rosterchirp_admin_setup_shown')) { + localStorage.setItem('rosterchirp_admin_setup_shown', '1'); + setModal('settings'); } else { setModal(null); } - }, [addChildPending]); + }, [addChildPending, helpDismissed, user]); // Register / refresh push subscription — FCM for Android/Chrome, Web Push for iOS useEffect(() => { diff --git a/frontend/src/pages/GroupManagerPage.jsx b/frontend/src/pages/GroupManagerPage.jsx index 3f05f7a..ecd6e73 100644 --- a/frontend/src/pages/GroupManagerPage.jsx +++ b/frontend/src/pages/GroupManagerPage.jsx @@ -759,7 +759,7 @@ export default function GroupManagerPage({ isMobile = false, onProfile, onHelp, const onRefresh = () => setRefreshKey(k => k+1); useEffect(() => { - api.searchUsers('').then(({ users }) => setAllUsers(users.filter(u => u.status==='active').sort((a, b) => (a.display_name||a.name).localeCompare(b.display_name||b.name)))).catch(() => {}); + api.searchUsers('').then(({ users }) => setAllUsers(users.filter(u => u.status==='active' && !u.is_default_admin).sort((a, b) => (a.display_name||a.name).localeCompare(b.display_name||b.name)))).catch(() => {}); api.getUserGroups().then(({ groups }) => setAllUserGroups([...(groups||[])].sort((a, b) => a.name.localeCompare(b.name)))).catch(() => {}); api.getSettings().then(({ settings }) => { const pgid = (settings || []).find(s => s.key === 'feature_players_group_id')?.value;