v0.12.24 minor user manager buf fixes
This commit is contained in:
@@ -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": {
|
||||||
|
|||||||
3
backend/src/models/migrations/010_dob_guardian.sql
Normal file
3
backend/src/models/migrations/010_dob_guardian.sql
Normal 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;
|
||||||
2
build.sh
2
build.sh
@@ -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"
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user