minor bug fixes

This commit is contained in:
2026-04-02 15:22:38 -04:00
parent 97b308e9f0
commit 18e4a92241
4 changed files with 79 additions and 29 deletions

View File

@@ -14,12 +14,13 @@ function isValidPhone(p) {
return /^\d{7,15}$/.test(digits);
}
// Format: email,firstname,lastname,password,role,usergroup (exactly 5 commas / 6 fields)
function parseCSV(text, ignoreFirstRow, allUserGroups) {
// Format: email,firstname,lastname,dob,password,role,usergroup (exactly 6 commas / 7 fields)
function parseCSV(text, ignoreFirstRow, allUserGroups, loginType) {
const lines = text.split(/\r?\n/).map(l => l.trim()).filter(Boolean);
const rows = [], invalid = [];
const groupMap = new Map((allUserGroups || []).map(g => [g.name.toLowerCase(), g]));
const validRoles = ['member', 'manager', 'admin'];
const requireDob = loginType === 'mixed_age';
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
@@ -27,12 +28,13 @@ function parseCSV(text, ignoreFirstRow, allUserGroups) {
if (i === 0 && (ignoreFirstRow || /^e-?mail$/i.test(line.split(',')[0].trim()))) continue;
const parts = line.split(',');
if (parts.length !== 6) { invalid.push({ line, reason: `Must have exactly 5 commas (has ${parts.length - 1})` }); continue; }
const [email, firstName, lastName, password, roleRaw, usergroupRaw] = parts.map(p => p.trim());
if (parts.length !== 7) { invalid.push({ line, reason: `Must have exactly 6 commas (has ${parts.length - 1})` }); continue; }
const [email, firstName, lastName, dobRaw, password, roleRaw, usergroupRaw] = parts.map(p => p.trim());
if (!email || !isValidEmail(email)) { invalid.push({ line, reason: `Invalid email: "${email || '(blank)'}"` }); continue; }
if (!firstName) { invalid.push({ line, reason: 'First name required' }); continue; }
if (!lastName) { invalid.push({ line, reason: 'Last name required' }); continue; }
if (requireDob && !dobRaw) { invalid.push({ line, reason: 'Date of birth required in Restricted login type' }); continue; }
const role = validRoles.includes(roleRaw.toLowerCase()) ? roleRaw.toLowerCase() : 'member';
const matchedGroup = usergroupRaw ? groupMap.get(usergroupRaw.toLowerCase()) : null;
@@ -42,6 +44,7 @@ function parseCSV(text, ignoreFirstRow, allUserGroups) {
firstName,
lastName,
password,
dateOfBirth: dobRaw || null,
role,
userGroupId: matchedGroup?.id || null,
userGroupName: usergroupRaw || null,
@@ -165,7 +168,6 @@ function UserForm({ user, userPass, allUserGroups, nonMinorUsers, loginType, onD
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');
@@ -286,7 +288,7 @@ function UserForm({ user, userPass, allUserGroups, nonMinorUsers, loginType, onD
{/* Row 4: DOB + Guardian */}
<div style={{ display:'grid', gridTemplateColumns:colGrid, gap:12, marginBottom:12 }}>
<div>
{lbl('Date of Birth', loginType === 'mixed_age', loginType !== 'mixed_age' ? '(optional)' : undefined)}
{lbl('Date of Birth', false, '(optional)')}
<input className="input" type="text" placeholder="YYYY-MM-DD"
value={dob} onChange={e => setDob(e.target.value)}
autoComplete="off" onFocus={onIF} onBlur={onIB} />
@@ -388,7 +390,7 @@ function UserForm({ user, userPass, allUserGroups, nonMinorUsers, loginType, onD
}
// ── Bulk Import Form ──────────────────────────────────────────────────────────
function BulkImportForm({ userPass, allUserGroups, onCreated }) {
function BulkImportForm({ userPass, allUserGroups, loginType, onCreated }) {
const toast = useToast();
const fileRef = useRef(null);
const [csvFile, setCsvFile] = useState(null);
@@ -403,9 +405,9 @@ function BulkImportForm({ userPass, allUserGroups, onCreated }) {
// Re-parse whenever raw text or options change
useEffect(() => {
if (!rawText) return;
const { rows, invalid } = parseCSV(rawText, ignoreFirst, allUserGroups);
const { rows, invalid } = parseCSV(rawText, ignoreFirst, allUserGroups, loginType);
setCsvRows(rows); setCsvInvalid(invalid);
}, [rawText, ignoreFirst, allUserGroups]);
}, [rawText, ignoreFirst, allUserGroups, loginType]);
const handleFile = e => {
const file = e.target.files?.[0]; if (!file) return;
@@ -435,11 +437,11 @@ function BulkImportForm({ userPass, allUserGroups, onCreated }) {
{/* Format info box */}
<div style={{ background:'var(--background)', border:'1px dashed var(--border)', borderRadius:'var(--radius)', padding:'12px 14px' }}>
<p style={{ fontSize:13, fontWeight:600, marginBottom:8 }}>CSV Format</p>
<code style={codeStyle}>{'FULL: email,firstname,lastname,password,role,usergroup'}</code>
<code style={codeStyle}>{'MINIMUM: email,firstname,lastname,,,'}</code>
<code style={codeStyle}>{'FULL: email,firstname,lastname,dob,password,role,usergroup'}</code>
<code style={codeStyle}>{'MINIMUM: email,firstname,lastname,,,,'}</code>
<p style={{ fontSize:12, color:'var(--text-tertiary)', margin:'8px 0 6px' }}>Examples:</p>
<code style={codeStyle}>{'example@rosterchirp.com,Barney,Rubble,,member,parents'}</code>
<code style={codeStyle}>{'example@rosterchirp.com,Barney,Rubble,Ori0n2026!,member,players'}</code>
<code style={codeStyle}>{'example@rosterchirp.com,Barney,Rubble,1970-11-21,,member,parents'}</code>
<code style={codeStyle}>{'example@rosterchirp.com,Barney,Rubble,2013-06-11,Ori0n2026!,member,players'}</code>
<p style={{ fontSize:12, color:'var(--text-tertiary)', marginTop:8 }}>
Blank password defaults to <strong>{userPass}</strong>. Blank role defaults to member. We recommend using a spreadsheet editor and saving as CSV.
</p>
@@ -455,8 +457,8 @@ function BulkImportForm({ userPass, allUserGroups, onCreated }) {
<div>
<p style={{ fontWeight:600, marginBottom:4 }}>CSV Requirements</p>
<ul style={{ paddingLeft:16, margin:0, lineHeight:1.8 }}>
<li>Exactly 5 commas per row (rows with more or less will be skipped)</li>
<li><code>email</code>, <code>firstname</code>, <code>lastname</code> are required</li>
<li>Exactly six (6) commas per row (rows with more or less will be skipped)</li>
<li><code>email</code>, <code>firstname</code>, <code>lastname</code> are required fields{loginType === 'mixed_age' ? <> (DOB field required for <strong>Restricted</strong> login type)</> : ''}.</li>
<li>A user can only be added to one group during bulk import</li>
<li>Optional fields left blank will use system defaults</li>
</ul>
@@ -711,7 +713,7 @@ export default function UserManagerPage({ isMobile = false, onProfile, onHelp, o
{/* BULK IMPORT */}
{view === 'bulk' && (
<div style={{ flex:1, overflowY:'auto', padding:16, paddingBottom: isMobile ? 'calc(82px + env(safe-area-inset-bottom, 0px))' : 16, overscrollBehavior:'contain' }}>
<BulkImportForm userPass={userPass} allUserGroups={allUserGroups} onCreated={load} />
<BulkImportForm userPass={userPass} allUserGroups={allUserGroups} loginType={loginType} onCreated={load} />
</div>
)}
</div>