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