v0.12.43 minor protection added
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user