minor bug fixes
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user