diff --git a/backend/package.json b/backend/package.json index 49cf63c..4730176 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "rosterchirp-backend", - "version": "0.12.52", + "version": "0.12.53", "description": "RosterChirp backend server", "main": "src/index.js", "scripts": { diff --git a/backend/src/routes/users.js b/backend/src/routes/users.js index 4efd468..d7ce2d1 100644 --- a/backend/src/routes/users.js +++ b/backend/src/routes/users.js @@ -192,6 +192,18 @@ router.patch('/:id', authMiddleware, teamManagerMiddleware, async (req, res) => const sgId = await getOrCreateSupportGroup(req.schema); if (sgId) await exec(req.schema, 'INSERT INTO group_members (group_id,user_id) VALUES ($1,$2) ON CONFLICT DO NOTHING', [sgId, id]); } + // Auto-unsuspend minor in players group if both guardian and DOB are now set + if (isMinor && guardianId && dob && target.status === 'suspended') { + const playersRow = await queryOne(req.schema, "SELECT value FROM settings WHERE key='feature_players_group_id'"); + const playersGroupId = parseInt(playersRow?.value); + if (playersGroupId) { + const inPlayers = await queryOne(req.schema, 'SELECT 1 FROM user_group_members WHERE user_id=$1 AND user_group_id=$2', [id, playersGroupId]); + if (inPlayers) { + await exec(req.schema, "UPDATE users SET status='active',updated_at=NOW() WHERE id=$1", [id]); + await addUserToPublicGroups(req.schema, id); + } + } + } const user = await queryOne(req.schema, 'SELECT id,name,first_name,last_name,phone,is_minor,date_of_birth,guardian_user_id,guardian_approval_required,email,role,status,must_change_password,last_online,created_at FROM users WHERE id=$1', [id] @@ -713,19 +725,25 @@ router.get('/minor-players', authMiddleware, async (req, res) => { }); // Claim minor as guardian (Mixed Age — Family Manager direct link, no approval needed) +// dateOfBirth is required to activate the minor — without it the guardian is saved but the account stays suspended. router.post('/me/guardian-children/:minorId', authMiddleware, async (req, res) => { const minorId = parseInt(req.params.minorId); + const { dateOfBirth } = req.body; try { const minor = await queryOne(req.schema, "SELECT * FROM users WHERE id=$1 AND status!='deleted'", [minorId]); if (!minor) return res.status(404).json({ error: 'User not found' }); if (!minor.is_minor) return res.status(400).json({ error: 'User is not a minor' }); if (minor.guardian_user_id && minor.guardian_user_id !== req.user.id) return res.status(409).json({ error: 'This minor already has a guardian' }); + const dob = dateOfBirth || minor.date_of_birth || null; + const isMinor = dob ? isMinorFromDOB(dob) : minor.is_minor; + const shouldActivate = !!dob; + const newStatus = shouldActivate ? 'active' : 'suspended'; await exec(req.schema, - "UPDATE users SET guardian_user_id=$1,guardian_approval_required=FALSE,status='active',updated_at=NOW() WHERE id=$2", - [req.user.id, minorId] + 'UPDATE users SET guardian_user_id=$1,guardian_approval_required=FALSE,date_of_birth=$2,is_minor=$3,status=$4,updated_at=NOW() WHERE id=$5', + [req.user.id, dob, isMinor, newStatus, minorId] ); - await addUserToPublicGroups(req.schema, minorId); + if (shouldActivate) await addUserToPublicGroups(req.schema, minorId); const user = await queryOne(req.schema, 'SELECT id,name,first_name,last_name,date_of_birth,avatar,status,guardian_user_id FROM users WHERE id=$1', [minorId] diff --git a/build.sh b/build.sh index bf94264..e1c2837 100644 --- a/build.sh +++ b/build.sh @@ -13,7 +13,7 @@ # ───────────────────────────────────────────────────────────── set -euo pipefail -VERSION="${1:-0.12.52}" +VERSION="${1:-0.12.53}" ACTION="${2:-}" REGISTRY="${REGISTRY:-}" IMAGE_NAME="rosterchirp" diff --git a/frontend/package.json b/frontend/package.json index e35d978..5244088 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "rosterchirp-frontend", - "version": "0.12.52", + "version": "0.12.53", "private": true, "scripts": { "dev": "vite", diff --git a/frontend/src/components/AddChildAliasModal.jsx b/frontend/src/components/AddChildAliasModal.jsx index 2fe6007..9bb8c71 100644 --- a/frontend/src/components/AddChildAliasModal.jsx +++ b/frontend/src/components/AddChildAliasModal.jsx @@ -19,6 +19,7 @@ export default function AddChildAliasModal({ features = {}, onClose }) { // ── Mixed-age state (real minor users) ──────────────────────────────────── const [minorPlayers, setMinorPlayers] = useState([]); // available + already-mine const [selectedMinorId, setSelectedMinorId] = useState(''); + const [childDob, setChildDob] = useState(''); const [addingMinor, setAddingMinor] = useState(false); // ── Partner state (shared) ──────────────────────────────────────────────── @@ -49,6 +50,13 @@ export default function AddChildAliasModal({ features = {}, onClose }) { }).catch(() => {}); }, [isMixedAge]); + // Pre-populate DOB when a minor is selected from the dropdown + useEffect(() => { + if (!selectedMinorId) { setChildDob(''); return; } + const minor = availableMinors.find(u => u.id === parseInt(selectedMinorId)); + setChildDob(minor?.date_of_birth ? minor.date_of_birth.slice(0, 10) : ''); + }, [selectedMinorId]); // eslint-disable-line react-hooks/exhaustive-deps + // ── Helpers ─────────────────────────────────────────────────────────────── const set = k => e => setForm(p => ({ ...p, [k]: e.target.value })); @@ -164,12 +172,14 @@ export default function AddChildAliasModal({ features = {}, onClose }) { const handleAddMinor = async () => { if (!selectedMinorId) return; + if (!childDob.trim()) return toast('Date of Birth is required', 'error'); setAddingMinor(true); try { - await api.addGuardianChild(parseInt(selectedMinorId)); + await api.addGuardianChild(parseInt(selectedMinorId), childDob.trim()); const { users: fresh } = await api.getMinorPlayers(); setMinorPlayers(fresh || []); setSelectedMinorId(''); + setChildDob(''); toast('Child added and account activated', 'success'); } catch (e) { toast(e.message, 'error'); @@ -282,24 +292,36 @@ export default function AddChildAliasModal({ features = {}, onClose }) {