v0.12.43 minor protection added

This commit is contained in:
2026-03-30 16:02:09 -04:00
parent e8e941c436
commit fe836ae69f
18 changed files with 1132 additions and 105 deletions

View File

@@ -158,6 +158,121 @@ function TeamManagementTab() {
);
}
// ── Login Type Tab ────────────────────────────────────────────────────────────
const LOGIN_TYPE_OPTIONS = [
{
id: 'all_ages',
label: 'All Ages',
desc: 'No age restrictions. All users interact normally. Default behaviour.',
},
{
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.",
},
{
id: 'mixed_age',
label: 'Mixed Age',
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.",
},
];
function LoginTypeTab() {
const toast = useToast();
const [loginType, setLoginType] = useState('all_ages');
const [playersGroupId, setPlayersGroupId] = useState('');
const [guardiansGroupId,setGuardiansGroupId] = useState('');
const [userGroups, setUserGroups] = useState([]);
const [canChange, setCanChange] = useState(false);
const [saving, setSaving] = useState(false);
useEffect(() => {
Promise.all([api.getSettings(), api.getUserGroups()]).then(([{ settings: s }, { groups }]) => {
setLoginType(s.feature_login_type || 'all_ages');
setPlayersGroupId(s.feature_players_group_id || '');
setGuardiansGroupId(s.feature_guardians_group_id || '');
setUserGroups([...(groups || [])].sort((a, b) => a.name.localeCompare(b.name)));
}).catch(() => {});
// Determine if the user table is empty enough to allow changes
api.getUsers().then(({ users }) => {
const nonAdmins = (users || []).filter(u => u.role !== 'admin');
setCanChange(nonAdmins.length === 0);
}).catch(() => {});
}, []);
const handleSave = async () => {
setSaving(true);
try {
await api.updateLoginType({
loginType,
playersGroupId: playersGroupId ? parseInt(playersGroupId) : null,
guardiansGroupId: guardiansGroupId ? parseInt(guardiansGroupId) : null,
});
toast('Login Type settings saved', 'success');
window.dispatchEvent(new Event('rosterchirp:settings-changed'));
} catch (e) { toast(e.message, 'error'); }
finally { setSaving(false); }
};
const needsGroups = loginType !== 'all_ages';
return (
<div>
<div className="settings-section-label">Login Type</div>
{/* Warning */}
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 8, background: 'var(--surface-variant)', border: '1px solid var(--border)', borderRadius: 'var(--radius)', padding: '10px 14px', marginBottom: 16 }}>
<span style={{ fontSize: 16, lineHeight: 1 }}></span>
<p style={{ fontSize: 12, color: 'var(--text-secondary)', margin: 0, lineHeight: 1.5 }}>
This setting can only be set or changed when the user table is empty (no non-admin users exist).
</p>
</div>
{/* Options */}
<div style={{ border: '1px solid var(--border)', borderRadius: 'var(--radius)', overflow: 'hidden', marginBottom: 16 }}>
{LOGIN_TYPE_OPTIONS.map((opt, i) => (
<label key={opt.id} style={{ display: 'flex', alignItems: 'flex-start', gap: 12, padding: '12px 14px', borderBottom: i < LOGIN_TYPE_OPTIONS.length - 1 ? '1px solid var(--border)' : 'none', cursor: canChange ? 'pointer' : 'not-allowed', opacity: canChange ? 1 : 0.6 }}>
<input type="radio" name="loginType" value={opt.id} checked={loginType === opt.id} disabled={!canChange}
onChange={() => setLoginType(opt.id)} style={{ marginTop: 3, accentColor: 'var(--primary)' }} />
<div>
<div style={{ fontSize: 14, fontWeight: 500 }}>{opt.label}</div>
<div style={{ fontSize: 12, color: 'var(--text-tertiary)', marginTop: 2, lineHeight: 1.5 }}>{opt.desc}</div>
</div>
</label>
))}
</div>
{/* Group selectors — only shown for Guardian Only / Mixed Age */}
{needsGroups && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, marginBottom: 16 }}>
<div>
<label className="text-sm font-medium" style={{ color: 'var(--text-secondary)', display: 'block', marginBottom: 4 }}>Players Group</label>
<p style={{ fontSize: 12, color: 'var(--text-tertiary)', marginBottom: 6 }}>The user group that children / aliases are added to.</p>
<select className="input" value={playersGroupId} disabled={!canChange}
onChange={e => setPlayersGroupId(e.target.value)}>
<option value=""> Select group </option>
{userGroups.map(g => <option key={g.id} value={g.id}>{g.name}</option>)}
</select>
</div>
<div>
<label className="text-sm font-medium" style={{ color: 'var(--text-secondary)', display: 'block', marginBottom: 4 }}>Guardians Group</label>
<p style={{ fontSize: 12, color: 'var(--text-tertiary)', marginBottom: 6 }}>Members of this group see the "Add Child" option in their profile.</p>
<select className="input" value={guardiansGroupId} disabled={!canChange}
onChange={e => setGuardiansGroupId(e.target.value)}>
<option value=""> Select group </option>
{userGroups.map(g => <option key={g.id} value={g.id}>{g.name}</option>)}
</select>
</div>
</div>
)}
<button className="btn btn-primary" onClick={handleSave} disabled={saving || !canChange}>
{saving ? 'Saving…' : 'Save'}
</button>
</div>
);
}
// ── Registration Tab ──────────────────────────────────────────────────────────
function RegistrationTab({ onFeaturesChanged }) {
const toast = useToast();
@@ -295,12 +410,6 @@ export default function SettingsModal({ onClose, onFeaturesChanged }) {
const isTeam = appType === 'RosterChirp-Team';
const tabs = [
{ id: 'messages', label: 'Messages' },
isTeam && { id: 'team', label: 'Tools' },
{ id: 'registration', label: 'Registration' },
].filter(Boolean);
return (
<div className="modal-overlay" onClick={e => e.target === e.currentTarget && onClose()}>
<div className="modal" style={{ maxWidth: 520 }}>
@@ -311,17 +420,20 @@ export default function SettingsModal({ onClose, onFeaturesChanged }) {
</button>
</div>
{/* Tab buttons */}
<div className="flex gap-2" style={{ marginBottom: 24 }}>
{tabs.map(t => (
<button key={t.id} className={`btn btn-sm ${tab === t.id ? 'btn-primary' : 'btn-secondary'}`} onClick={() => setTab(t.id)}>
{t.label}
</button>
))}
{/* Select navigation */}
<div style={{ marginBottom: 24 }}>
<label className="text-sm" style={{ color: 'var(--text-tertiary)', display: 'block', marginBottom: 4 }}>SELECT OPTION:</label>
<select className="input" value={tab} onChange={e => setTab(e.target.value)}>
<option value="messages">Messages</option>
{isTeam && <option value="team">Tools</option>}
<option value="login-type">Login Type</option>
<option value="registration">Registration</option>
</select>
</div>
{tab === 'messages' && <MessagesTab />}
{tab === 'team' && <TeamManagementTab />}
{tab === 'login-type' && <LoginTypeTab />}
{tab === 'registration' && <RegistrationTab onFeaturesChanged={onFeaturesChanged} />}
</div>
</div>