v0.12.24 minor user manager buf fixes

This commit is contained in:
2026-03-24 16:10:27 -04:00
parent e77176841c
commit 7276228a98
5 changed files with 32 additions and 25 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "rosterchirp-backend", "name": "rosterchirp-backend",
"version": "0.12.23", "version": "0.12.24",
"description": "RosterChirp backend server", "description": "RosterChirp backend server",
"main": "src/index.js", "main": "src/index.js",
"scripts": { "scripts": {

View File

@@ -0,0 +1,3 @@
-- Migration 010: Date of birth and guardian fields
ALTER TABLE users ADD COLUMN IF NOT EXISTS date_of_birth DATE;
ALTER TABLE users ADD COLUMN IF NOT EXISTS guardian_user_id INTEGER REFERENCES users(id) ON DELETE SET NULL;

View File

@@ -13,7 +13,7 @@
# ───────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────
set -euo pipefail set -euo pipefail
VERSION="${1:-0.12.23}" VERSION="${1:-0.12.24}"
ACTION="${2:-}" ACTION="${2:-}"
REGISTRY="${REGISTRY:-}" REGISTRY="${REGISTRY:-}"
IMAGE_NAME="rosterchirp" IMAGE_NAME="rosterchirp"

View File

@@ -1,6 +1,6 @@
{ {
"name": "rosterchirp-frontend", "name": "rosterchirp-frontend",
"version": "0.12.23", "version": "0.12.24",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@@ -119,9 +119,10 @@ function UserForm({ user, userPass, allUserGroups, onDone, onCancel, isMobile, o
const [lastName, setLastName] = useState(user?.last_name || ''); const [lastName, setLastName] = useState(user?.last_name || '');
const [email, setEmail] = useState(user?.email || ''); const [email, setEmail] = useState(user?.email || '');
const [phone, setPhone] = useState(user?.phone || ''); const [phone, setPhone] = useState(user?.phone || '');
const [role, setRole] = useState(user?.role || 'member'); const [role, setRole] = useState(user?.role || 'member');
const [isMinor, setIsMinor] = useState(!!user?.is_minor); const [dob, setDob] = useState(user?.date_of_birth || '');
const [password, setPassword] = useState(''); const [guardianId, setGuardianId] = useState(user?.guardian_user_id || '');
const [password, setPassword] = useState('');
const [pwEnabled, setPwEnabled] = useState(!isEdit); const [pwEnabled, setPwEnabled] = useState(!isEdit);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [selectedGroupIds, setSelectedGroupIds] = useState(new Set()); const [selectedGroupIds, setSelectedGroupIds] = useState(new Set());
@@ -155,8 +156,6 @@ function UserForm({ user, userPass, allUserGroups, onDone, onCancel, isMobile, o
if (!lastName.trim()) return toast('Last name is required', 'error'); if (!lastName.trim()) return toast('Last name is required', 'error');
if (!isValidPhone(phone)) return toast('Invalid phone number', 'error'); if (!isValidPhone(phone)) return toast('Invalid phone number', 'error');
if (!['member', 'admin', 'manager'].includes(role)) return toast('Role is required', 'error'); if (!['member', 'admin', 'manager'].includes(role)) return toast('Role is required', 'error');
if (!isEdit && (!password || password.length < 6))
return toast('Password must be at least 6 characters', 'error');
if (isEdit && pwEnabled && (!password || password.length < 6)) if (isEdit && pwEnabled && (!password || password.length < 6))
return toast('New password must be at least 6 characters', 'error'); return toast('New password must be at least 6 characters', 'error');
@@ -167,7 +166,6 @@ function UserForm({ user, userPass, allUserGroups, onDone, onCancel, isMobile, o
firstName: firstName.trim(), firstName: firstName.trim(),
lastName: lastName.trim(), lastName: lastName.trim(),
phone: phone.trim(), phone: phone.trim(),
isMinor,
role, role,
...(pwEnabled && password ? { password } : {}), ...(pwEnabled && password ? { password } : {}),
}); });
@@ -185,9 +183,8 @@ function UserForm({ user, userPass, allUserGroups, onDone, onCancel, isMobile, o
lastName: lastName.trim(), lastName: lastName.trim(),
email: email.trim(), email: email.trim(),
phone: phone.trim(), phone: phone.trim(),
isMinor,
role, role,
password, ...(password ? { password } : {}),
}); });
// Add to selected groups // Add to selected groups
for (const gId of selectedGroupIds) { for (const gId of selectedGroupIds) {
@@ -273,13 +270,23 @@ function UserForm({ user, userPass, allUserGroups, onDone, onCancel, isMobile, o
</div> </div>
</div> </div>
{/* Row 4: Is minor */} {/* Row 4: DOB + Guardian */}
<div style={{ marginBottom:12 }}> <div style={{ display:'grid', gridTemplateColumns:colGrid, gap:12, marginBottom:12 }}>
<label style={{ display:'flex', alignItems:'center', gap:8, cursor:'pointer', fontSize:14, color:'var(--text-primary)', userSelect:'none' }}> <div>
<input type="checkbox" checked={isMinor} onChange={e => setIsMinor(e.target.checked)} {lbl('Date of Birth', false, '(optional)')}
style={{ accentColor:'var(--primary)', width:15, height:15 }} /> <input className="input" type="text" placeholder="YYYY-MM-DD"
User is a minor value={dob} onChange={e => setDob(e.target.value)}
</label> disabled
style={{ opacity:0.5, cursor:'not-allowed' }} />
</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> </div>
{/* Row 4b: User Groups */} {/* Row 4b: User Groups */}
@@ -303,9 +310,9 @@ function UserForm({ user, userPass, allUserGroups, onDone, onCancel, isMobile, o
{/* Row 5: Password */} {/* Row 5: Password */}
<div style={{ marginBottom:16 }}> <div style={{ marginBottom:16 }}>
{lbl('Password', {lbl('Password',
(!isEdit) || (isEdit && pwEnabled), isEdit && pwEnabled,
isEdit && !pwEnabled ? '(not changing — click Reset Password to set a new one)' : isEdit && !pwEnabled ? '(not changing — click Reset Password to set a new one)' :
!isEdit ? `(blank = ${userPass})` : null !isEdit ? `(optional — blank uses system default)` : null
)} )}
<div style={{ opacity: pwEnabled ? 1 : 0.55 }}> <div style={{ opacity: pwEnabled ? 1 : 0.55 }}>
<PasswordInput <PasswordInput
@@ -335,9 +342,6 @@ function UserForm({ user, userPass, allUserGroups, onDone, onCancel, isMobile, o
Cancel Reset Cancel Reset
</button> </button>
)} )}
<button className="btn btn-secondary" onClick={onCancel} style={{ marginLeft:'auto' }}>
Cancel
</button>
</div> </div>
{/* Row 7 (edit only): Last login + must change password */} {/* Row 7 (edit only): Last login + must change password */}
@@ -393,7 +397,7 @@ function BulkImportForm({ userPass, allUserGroups, onCreated }) {
finally { setLoading(false); } finally { setLoading(false); }
}; };
const codeStyle = { fontSize:12, color:'var(--text-secondary)', display:'block', background:'var(--surface)', padding:'6px 8px', borderRadius:4, border:'1px solid var(--border)', whiteSpace:'pre', fontFamily:'monospace', marginBottom:4 }; const codeStyle = { fontSize:12, color:'var(--text-secondary)', display:'block', background:'var(--surface)', padding:'6px 8px', borderRadius:4, border:'1px solid var(--border)', whiteSpace:'pre-wrap', overflowWrap:'anywhere', fontFamily:'monospace', marginBottom:4 };
return ( return (
<div style={{ maxWidth:580, display:'flex', flexDirection:'column', gap:16 }}> <div style={{ maxWidth:580, display:'flex', flexDirection:'column', gap:16 }}>
@@ -444,7 +448,7 @@ function BulkImportForm({ userPass, allUserGroups, onCreated }) {
</ul> </ul>
</div> </div>
<p style={{ color:'var(--text-tertiary)', marginTop:2 }}> <p style={{ color:'var(--text-tertiary)', marginTop:2 }}>
Optional field defaults: password = <strong>{userPass}</strong>, role = member, usergroup = (none), minor = (none) Optional field defaults: password = <strong>{userPass}</strong>, role = member, usergroup = (none)
</p> </p>
</div> </div>
)} )}