diff --git a/backend/package.json b/backend/package.json index c9dadab..caf76a6 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "rosterchirp-backend", - "version": "0.12.23", + "version": "0.12.24", "description": "RosterChirp backend server", "main": "src/index.js", "scripts": { diff --git a/backend/src/models/migrations/010_dob_guardian.sql b/backend/src/models/migrations/010_dob_guardian.sql new file mode 100644 index 0000000..e5e0312 --- /dev/null +++ b/backend/src/models/migrations/010_dob_guardian.sql @@ -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; diff --git a/build.sh b/build.sh index 471c202..46b9e9a 100644 --- a/build.sh +++ b/build.sh @@ -13,7 +13,7 @@ # ───────────────────────────────────────────────────────────── set -euo pipefail -VERSION="${1:-0.12.23}" +VERSION="${1:-0.12.24}" ACTION="${2:-}" REGISTRY="${REGISTRY:-}" IMAGE_NAME="rosterchirp" diff --git a/frontend/package.json b/frontend/package.json index a533eaf..c583eec 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "rosterchirp-frontend", - "version": "0.12.23", + "version": "0.12.24", "private": true, "scripts": { "dev": "vite", diff --git a/frontend/src/pages/UserManagerPage.jsx b/frontend/src/pages/UserManagerPage.jsx index 9b4b194..2022479 100644 --- a/frontend/src/pages/UserManagerPage.jsx +++ b/frontend/src/pages/UserManagerPage.jsx @@ -119,9 +119,10 @@ function UserForm({ user, userPass, allUserGroups, onDone, onCancel, isMobile, o const [lastName, setLastName] = useState(user?.last_name || ''); const [email, setEmail] = useState(user?.email || ''); const [phone, setPhone] = useState(user?.phone || ''); - const [role, setRole] = useState(user?.role || 'member'); - const [isMinor, setIsMinor] = useState(!!user?.is_minor); - const [password, setPassword] = useState(''); + const [role, setRole] = useState(user?.role || 'member'); + const [dob, setDob] = useState(user?.date_of_birth || ''); + const [guardianId, setGuardianId] = useState(user?.guardian_user_id || ''); + const [password, setPassword] = useState(''); const [pwEnabled, setPwEnabled] = useState(!isEdit); const [saving, setSaving] = useState(false); 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 (!isValidPhone(phone)) return toast('Invalid phone number', '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)) 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(), lastName: lastName.trim(), phone: phone.trim(), - isMinor, role, ...(pwEnabled && password ? { password } : {}), }); @@ -185,9 +183,8 @@ function UserForm({ user, userPass, allUserGroups, onDone, onCancel, isMobile, o lastName: lastName.trim(), email: email.trim(), phone: phone.trim(), - isMinor, role, - password, + ...(password ? { password } : {}), }); // Add to selected groups for (const gId of selectedGroupIds) { @@ -273,13 +270,23 @@ function UserForm({ user, userPass, allUserGroups, onDone, onCancel, isMobile, o - {/* Row 4: Is minor */} -
- + {/* Row 4: DOB + Guardian */} +
+
+ {lbl('Date of Birth', false, '(optional)')} + setDob(e.target.value)} + disabled + style={{ opacity:0.5, cursor:'not-allowed' }} /> +
+
+ {lbl('Guardian', false, '(optional)')} + +
{/* Row 4b: User Groups */} @@ -303,9 +310,9 @@ function UserForm({ user, userPass, allUserGroups, onDone, onCancel, isMobile, o {/* Row 5: Password */}
{lbl('Password', - (!isEdit) || (isEdit && pwEnabled), + isEdit && pwEnabled, isEdit && !pwEnabled ? '(not changing — click Reset Password to set a new one)' : - !isEdit ? `(blank = ${userPass})` : null + !isEdit ? `(optional — blank uses system default)` : null )}
)} -
{/* Row 7 (edit only): Last login + must change password */} @@ -393,7 +397,7 @@ function BulkImportForm({ userPass, allUserGroups, onCreated }) { 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 (
@@ -444,7 +448,7 @@ function BulkImportForm({ userPass, allUserGroups, onCreated }) {

- Optional field defaults: password = {userPass}, role = member, usergroup = (none), minor = (none) + Optional field defaults: password = {userPass}, role = member, usergroup = (none)

)}