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

@@ -89,10 +89,11 @@ function UserRow({ u, onUpdated, onEdit }) {
<Avatar user={u} size="sm" />
<div style={{ flex:1, minWidth:0 }}>
<div style={{ display:'flex', alignItems:'center', gap:6, flexWrap:'wrap' }}>
<span style={{ fontWeight:600, fontSize:14 }}>{u.display_name || u.name}</span>
<span style={{ fontWeight:600, fontSize:14, color: u.guardian_approval_required ? 'var(--error)' : 'var(--text-primary)' }}>{u.display_name || u.name}</span>
{u.display_name && <span style={{ fontSize:12, color:'var(--text-tertiary)' }}>({u.name})</span>}
<span className={`role-badge role-${u.role}`}>{u.role}</span>
{u.status !== 'active' && <span className="role-badge status-suspended">{u.status}</span>}
{!!u.guardian_approval_required && <span className="role-badge" style={{ background:'var(--error)', color:'white' }}>Pending Guardian Approval</span>}
{!!u.is_default_admin && <span className="text-xs" style={{ color:'var(--text-tertiary)' }}>Default Admin</span>}
</div>
<div style={{ fontSize:12, color:'var(--text-secondary)', marginTop:1, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>{u.email}</div>
@@ -129,7 +130,7 @@ function UserRow({ u, onUpdated, onEdit }) {
}
// ── User Form (create / edit) ─────────────────────────────────────────────────
function UserForm({ user, userPass, allUserGroups, onDone, onCancel, isMobile, onIF, onIB }) {
function UserForm({ user, userPass, allUserGroups, nonMinorUsers, loginType, onDone, onCancel, isMobile, onIF, onIB }) {
const toast = useToast();
const isEdit = !!user;
@@ -164,6 +165,7 @@ function UserForm({ user, userPass, allUserGroups, onDone, onCancel, isMobile, o
if (!lastName.trim()) return toast('Last name is required', 'error');
if (!isValidPhone(phone)) return toast('Invalid phone number', 'error');
if (!['member', 'admin', 'manager'].includes(role)) return toast('Role is required', 'error');
if (loginType === 'mixed_age' && !dob) return toast('Date of birth is required in Mixed Age mode', 'error');
if (isEdit && pwEnabled && (!password || password.length < 6))
return toast('New password must be at least 6 characters', 'error');
@@ -171,10 +173,12 @@ function UserForm({ user, userPass, allUserGroups, onDone, onCancel, isMobile, o
try {
if (isEdit) {
await api.updateUser(user.id, {
firstName: firstName.trim(),
lastName: lastName.trim(),
phone: phone.trim(),
firstName: firstName.trim(),
lastName: lastName.trim(),
phone: phone.trim(),
role,
dateOfBirth: dob || undefined,
guardianUserId: guardianId || undefined,
...(pwEnabled && password ? { password } : {}),
});
// Sync group memberships: add newly selected, remove deselected
@@ -187,11 +191,12 @@ function UserForm({ user, userPass, allUserGroups, onDone, onCancel, isMobile, o
toast('User updated', 'success');
} else {
const { user: newUser } = await api.createUser({
firstName: firstName.trim(),
lastName: lastName.trim(),
email: email.trim(),
phone: phone.trim(),
firstName: firstName.trim(),
lastName: lastName.trim(),
email: email.trim(),
phone: phone.trim(),
role,
dateOfBirth: dob || undefined,
...(password ? { password } : {}),
});
// Add to selected groups
@@ -278,24 +283,42 @@ function UserForm({ user, userPass, allUserGroups, onDone, onCancel, isMobile, o
</div>
</div>
{/* Row 4: DOB + Guardian */}
<div style={{ display:'grid', gridTemplateColumns:colGrid, gap:12, marginBottom:12 }}>
<div>
{lbl('Date of Birth', false, '(optional)')}
<input className="input" type="text" placeholder="YYYY-MM-DD"
value={dob} onChange={e => setDob(e.target.value)}
disabled
style={{ opacity:0.5, cursor:'not-allowed' }} />
{/* Row 4: DOB + Guardian — visible when loginType is not 'all_ages' */}
{loginType !== 'all_ages' && (
<div style={{ display:'grid', gridTemplateColumns:colGrid, gap:12, marginBottom:12 }}>
<div>
{lbl('Date of Birth', loginType === 'mixed_age', loginType === 'guardian_only' ? '(optional)' : undefined)}
<input className="input" type="text" placeholder="YYYY-MM-DD"
value={dob} onChange={e => setDob(e.target.value)}
autoComplete="off" onFocus={onIF} onBlur={onIB} />
</div>
{loginType === 'mixed_age' && isEdit && (
<div>
{lbl('Guardian', false, '(optional)')}
<div style={{ position:'relative' }}>
<select className="input" value={guardianId} onChange={e => setGuardianId(e.target.value)}
style={ user?.guardian_approval_required ? { borderColor:'var(--error)' } : {} }>
<option value=""> None </option>
{(nonMinorUsers || []).map(u => <option key={u.id} value={u.id}>{u.name}</option>)}
</select>
</div>
{user?.guardian_approval_required && (
<div style={{ display:'flex', alignItems:'center', gap:8, marginTop:6 }}>
<span style={{ fontSize:12, color:'var(--error)', fontWeight:600 }}>Pending approval</span>
<button className="btn btn-sm" style={{ fontSize:12, color:'var(--success)', background:'none', border:'1px solid var(--success)', padding:'2px 8px', cursor:'pointer' }}
onClick={async () => { try { await api.approveGuardian(user.id); toast('Approved', 'success'); onDone(); } catch(e) { toast(e.message,'error'); } }}>
Approve
</button>
<button className="btn btn-sm" style={{ fontSize:12, color:'var(--error)', background:'none', border:'1px solid var(--error)', padding:'2px 8px', cursor:'pointer' }}
onClick={async () => { try { await api.denyGuardian(user.id); toast('Denied', 'success'); onDone(); } catch(e) { toast(e.message,'error'); } }}>
Deny
</button>
</div>
)}
</div>
)}
</div>
<div>
{lbl('Guardian', false, '(optional)')}
<select className="input" value={guardianId} onChange={e => setGuardianId(e.target.value)}
disabled
style={{ opacity:0.5, cursor:'not-allowed' }}>
<option value=""> Select guardian </option>
</select>
</div>
</div>
)}
{/* Row 4b: User Groups */}
{allUserGroups?.length > 0 && (
@@ -543,6 +566,7 @@ export default function UserManagerPage({ isMobile = false, onProfile, onHelp, o
const [editUser, setEditUser] = useState(null);
const [userPass, setUserPass] = useState('user@1234');
const [allUserGroups, setAllUserGroups] = useState([]);
const [loginType, setLoginType] = useState('all_ages');
const [inputFocused, setInputFocused] = useState(false);
const onIF = () => setInputFocused(true);
const onIB = () => setInputFocused(false);
@@ -556,7 +580,10 @@ export default function UserManagerPage({ isMobile = false, onProfile, onHelp, o
useEffect(() => {
load();
api.getSettings().then(({ settings }) => { if (settings.user_pass) setUserPass(settings.user_pass); }).catch(() => {});
api.getSettings().then(({ settings }) => {
if (settings.user_pass) setUserPass(settings.user_pass);
setLoginType(settings.feature_login_type || 'all_ages');
}).catch(() => {});
api.getUserGroups().then(({ groups }) => setAllUserGroups([...(groups||[])].sort((a,b) => a.name.localeCompare(b.name)))).catch(() => {});
}, [load]);
@@ -664,6 +691,8 @@ export default function UserManagerPage({ isMobile = false, onProfile, onHelp, o
user={view === 'edit' ? editUser : null}
userPass={userPass}
allUserGroups={allUserGroups}
nonMinorUsers={users.filter(u => !u.is_minor && u.status === 'active')}
loginType={loginType}
onDone={() => { load(); goList(); }}
onCancel={goList}
isMobile={isMobile}