import { useState, useEffect } from 'react'; import axios from 'axios'; import { Upload, FileText, AlertCircle, CheckCircle, Download, History } from 'lucide-react'; import AdminLayout from './AdminLayout.js'; export default function RosterUpload() { const [file, setFile] = useState(null); const [preview, setPreview] = useState([]); const [uploading, setUploading] = useState(false); const [error, setError] = useState(null); const [success, setSuccess] = useState(null); const [history, setHistory] = useState([]); const [loadingHistory, setLoadingHistory] = useState(true); const [activeTab, setActiveTab] = useState('upload'); // 'upload' | 'history' const [orgType, setOrgType] = useState(null); const [loadingOrgType, setLoadingOrgType] = useState(true); useEffect(() => { fetchHistory(); fetchOrgType(); }, []); const fetchHistory = async () => { try { const { data } = await axios.get('/api/admin/roster/history', { withCredentials: true }); setHistory(data); } catch (err) { console.error('Failed to load roster history:', err); } finally { setLoadingHistory(false); } }; const fetchOrgType = async () => { try { const { data } = await axios.get('/api/admin/organization/profile', { withCredentials: true }); setOrgType(data.organization_type); } catch (err) { console.error('Failed to load organization type:', err); setOrgType(''); // Default to empty if can't fetch } finally { setLoadingOrgType(false); } }; const handleFileChange = (e) => { const selectedFile = e.target.files[0]; if (!selectedFile) return; if (!selectedFile.name.endsWith('.csv')) { setError('Please select a CSV file'); return; } setFile(selectedFile); setError(null); setSuccess(null); // Parse CSV for preview const reader = new FileReader(); reader.onload = (event) => { const text = event.target.result; const lines = text.split('\n').filter(line => line.trim()); if (lines.length === 0) { setError('CSV file is empty'); setPreview([]); return; } // Parse header const headers = lines[0].split(',').map(h => h.trim().toLowerCase()); // Validate required columns (grade_level required for K-12 schools only) const requiredCols = orgType === 'K-12 School' ? ['email', 'firstname', 'lastname', 'grade_level'] : ['email', 'firstname', 'lastname']; const missing = requiredCols.filter(col => !headers.includes(col)); if (missing.length > 0) { setError(`Missing required columns: ${missing.join(', ')}`); setPreview([]); return; } // Parse rows (limit preview to first 10) const rows = []; for (let i = 1; i < Math.min(lines.length, 11); i++) { const values = lines[i].split(',').map(v => v.trim()); const row = {}; headers.forEach((header, idx) => { row[header] = values[idx] || ''; }); // Basic validation const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; let valid = emailRegex.test(row.email) && row.firstname && row.lastname; // For K-12 schools, grade_level is required and must be 9-12 if (orgType === 'K-12 School') { if (row.grade_level && row.grade_level.trim()) { const gradeLevel = parseInt(row.grade_level); valid = valid && !isNaN(gradeLevel) && gradeLevel >= 9 && gradeLevel <= 12; } else { valid = false; // Missing required grade_level } } else { // For non-K12, grade_level is optional but if provided must be 9-12 if (row.grade_level && row.grade_level.trim()) { const gradeLevel = parseInt(row.grade_level); valid = valid && !isNaN(gradeLevel) && gradeLevel >= 9 && gradeLevel <= 12; } } row.valid = valid; rows.push(row); } setPreview(rows); if (lines.length > 11) { setError(null); } }; reader.readAsText(selectedFile); }; const handleUpload = async () => { if (!file || preview.length === 0) { setError('Please select a file first'); return; } setUploading(true); setError(null); setSuccess(null); try { // Use the already-parsed preview data but parse entire file const text = await file.text(); const lines = text.split('\n').filter(line => line.trim()); const headers = lines[0].split(',').map(h => h.trim().toLowerCase()); // Parse ALL rows (not just preview) const students = []; for (let i = 1; i < lines.length; i++) { const values = lines[i].split(',').map(v => v.trim()); const student = {}; headers.forEach((header, idx) => { student[header] = values[idx] || ''; }); // Only include if has required fields if (student.email && student.firstname && student.lastname) { const studentData = { email: student.email, firstname: student.firstname, lastname: student.lastname, status: student.status || 'active' }; // Include grade_level only if provided and valid if (student.grade_level && student.grade_level.trim()) { const gradeLevel = parseInt(student.grade_level); if (!isNaN(gradeLevel) && gradeLevel >= 9 && gradeLevel <= 12) { studentData.grade_level = gradeLevel; } } students.push(studentData); } } if (students.length === 0) { setError('No valid students found in CSV'); setUploading(false); return; } // Send parsed JSON to backend const { data } = await axios.post('/api/admin/roster/upload', { students }, { withCredentials: true } ); setSuccess(`Successfully added ${data.results.added} students, updated ${data.results.updated} existing students${data.results.errors.length > 0 ? `, ${data.results.errors.length} errors` : ''}`); setFile(null); setPreview([]); // Refresh history fetchHistory(); // Reset file input const fileInput = document.getElementById('roster-file-input'); if (fileInput) fileInput.value = ''; } catch (err) { setError(err.response?.data?.error || 'Failed to upload roster'); } finally { setUploading(false); } }; const downloadTemplate = () => { const csv = orgType === 'K-12 School' ? 'email,firstname,lastname,grade_level\nstudent1@example.com,John,Doe,11\nstudent2@example.com,Jane,Smith,12' : 'email,firstname,lastname\nstudent1@example.com,John,Doe\nstudent2@example.com,Jane,Smith'; const blob = new Blob([csv], { type: 'text/csv' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'roster_template.csv'; a.click(); URL.revokeObjectURL(url); }; return (

Roster Management

Upload student rosters and view upload history

{/* Tabs */}
{/* Upload Tab */} {activeTab === 'upload' && (
{/* Instructions */}

CSV Format Requirements

  • • Required columns: email, firstname, lastname{orgType === 'K-12 School' && <>, grade_level}
  • {orgType === 'K-12 School' ? (
  • • Grade level must be 9-12 (9th-12th grade)
  • ) : (
  • • Optional column: grade_level (if provided, must be 9-12)
  • )}
  • • Header row must be included
  • • Email addresses must be valid format
  • • Duplicate emails will be skipped automatically
{/* File Upload */}
{error && (

{error}

)} {success && (

{success}

)} {preview.length > 0 && (

Preview ({preview.length} rows shown)

{preview.map((row, idx) => ( ))}
Email First Name Last Name Grade Status
{row.email} {row.firstname} {row.lastname} {row.grade_level} {row.valid ? ( Valid ) : ( Invalid )}
)}
)} {/* History Tab */} {activeTab === 'history' && (
{loadingHistory ? (

Loading history...

) : history.length === 0 ? (

No roster uploads yet

) : (
{history.map((upload) => ( ))}
Upload Date Students Added Already Existed Total Roster Size Change %
{new Date(upload.uploaded_at).toLocaleString()} {upload.students_added} {upload.students_existing} {upload.total_students_after} 0 ? 'text-green-600' : 'text-gray-400' }`}> {upload.change_percentage > 0 ? '+' : ''}{upload.change_percentage}%
)}
)}
); }