diff --git a/admin-client/src/pages/admin/MembersTab.jsx b/admin-client/src/pages/admin/MembersTab.jsx index 03a921f..b4eb120 100644 --- a/admin-client/src/pages/admin/MembersTab.jsx +++ b/admin-client/src/pages/admin/MembersTab.jsx @@ -15,6 +15,9 @@ const MembersTab = ({ initialClanFilter }) => { const [search, setSearch] = useState(''); const [levelFilter, setLevelFilter] = useState(''); const [clanFilter, setClanFilter] = useState(initialClanFilter || ''); + const [minSolved, setMinSolved] = useState(''); + const [maxSolved, setMaxSolved] = useState(''); + const [sortBy, setSortBy] = useState('Default'); const [menuOpen, setMenuOpen] = useState(null); const [menuPos, setMenuPos] = useState({ top: 0, right: 0 }); const canManageUsers = canManageClanGlobally(user); @@ -184,9 +187,55 @@ const MembersTab = ({ initialClanFilter }) => { } else if (clanFilter) { matchClan = activeClan?._id === clanFilter; } - return matchSearch && matchLevel && matchClan; + + const solved = u.solvedProblems || 0; + const matchesMin = minSolved === '' || solved >= parseInt(minSolved); + const matchesMax = maxSolved === '' || solved <= parseInt(maxSolved); + + return matchSearch && matchLevel && matchClan && matchesMin && matchesMax; + }); + + const sortedUsers = [...filteredUsers].sort((a, b) => { + const aSolved = a.solvedProblems || 0; + const bSolved = b.solvedProblems || 0; + if (sortBy === 'Solved (Low to High)') return aSolved - bSolved; + if (sortBy === 'Solved (High to Low)') return bSolved - aSolved; + return 0; // Default }); + const handleDownloadCSV = () => { + const headers = ['Name', 'Username', 'Email', 'Reg No', 'Clan', 'Level', 'Solved', 'XP', 'Streak', 'Status']; + const rows = sortedUsers.map(u => { + const activeClan = u.clan ? (clansQuery.data || []).find(c => c._id === u.clan || c._id === u.clan?._id) : null; + const clanName = activeClan ? activeClan.name : 'Unassigned'; + let status = 'Not Started'; + if ((u.solvedProblems || 0) > 0) status = 'In Progress'; // Can't easily determine 'Completed all' globally without total question count + + return [ + `"${u.name || ''}"`, + `"${u.username || ''}"`, + `"${u.email || ''}"`, + `"${u.regNo || 'N/A'}"`, + `"${clanName}"`, + `"${u.codingLevel || 'Beginner'}"`, + u.solvedProblems || 0, + u.points || 0, + u.streak || 0, + `"${u.status || 'Active'}"` + ].join(','); + }); + + const csvContent = [headers.join(','), ...rows].join('\n'); + const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.setAttribute('href', url); + link.setAttribute('download', `Admin_Directory_${new Date().toISOString().split('T')[0]}.csv`); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + }; + const getStatusDot = (status) => { switch(status) { case 'Active': return
; @@ -250,6 +299,44 @@ const MembersTab = ({ initialClanFilter }) => { ))}
+ +
+ Solved: + setMinSolved(e.target.value)} + className="w-12 bg-transparent text-sm text-black dark:text-white text-center focus:outline-none border-b border-transparent focus:border-blue-500/50" + /> + - + setMaxSolved(e.target.value)} + className="w-12 bg-transparent text-sm text-black dark:text-white text-center focus:outline-none border-b border-transparent focus:border-blue-500/50" + /> +
+ +
+ +
+ + @@ -352,7 +439,7 @@ const MembersTab = ({ initialClanFilter }) => { - {filteredUsers.map((user, i) => ( + {sortedUsers.map((user, i) => ( @@ -413,14 +500,14 @@ const MembersTab = ({ initialClanFilter }) => { ))} - {filteredUsers.length === 0 && ( + {sortedUsers.length === 0 && ( No members found matching the criteria. )}
- Showing {filteredUsers.length} members + Showing {sortedUsers.length} members
diff --git a/admin-client/src/pages/admin/QuestionSetsTab.jsx b/admin-client/src/pages/admin/QuestionSetsTab.jsx index ab08832..133d5db 100644 --- a/admin-client/src/pages/admin/QuestionSetsTab.jsx +++ b/admin-client/src/pages/admin/QuestionSetsTab.jsx @@ -291,6 +291,9 @@ const QuestionSetsTab = () => { const [solutionLangByKey, setSolutionLangByKey] = useState({}); const [previewQuestion, setPreviewQuestion] = useState(null); + const todayStr = new Date().toISOString().split('T')[0]; + const isPastDeadline = editingSetId && form.deadline && form.deadline < todayStr; + const blankForm = ()=>({title:'',weekNumber:1,deadline:'',targetLevel:'Both',questions:[{...initialQuestionState}]}); const setsQuery = useQuery({queryKey:['admin-question-sets'],queryFn:async()=>{try{const r=await api.get('/api/sets');return r.data.data||[]}catch{return[]}}}); @@ -496,38 +499,38 @@ const QuestionSetsTab = () => { ); }; - const HintsEditor = ({hints,onChange}) => { + const HintsEditor = ({hints,onChange,disabled}) => { const list = hints && hints.length ? hints : ['']; return (
- + {!disabled && }
{list.map((hint,hi)=>(
- {const nh=[...list];nh[hi]=e.target.value;onChange(nh)}}/> - {list.length>1&&} + {const nh=[...list];nh[hi]=e.target.value;onChange(nh)}}/> + {list.length>1&&!disabled&&}
))}
); }; - const TestCaseEditor = ({cases,onChange}) => ( + const TestCaseEditor = ({cases,onChange,disabled}) => (
- + {!disabled && }
{(!cases||cases.length===0)?

No test cases — click Add Case.

:(
{cases.map((tc,i)=>( -
-

Label

{const t=[...cases];t[i]={...t[i],label:e.target.value};onChange(t)}}/>
-

Args (JSON array)

{const t=[...cases];t[i]={...t[i],args:e.target.value};onChange(t)}}/>
-

Expected

{const t=[...cases];t[i]={...t[i],expected:e.target.value};onChange(t)}}/>
- +
+

Label

{const t=[...cases];t[i]={...t[i],label:e.target.value};onChange(t)}}/>
+

Args (JSON array)

{const t=[...cases];t[i]={...t[i],args:e.target.value};onChange(t)}}/>
+

Expected

{const t=[...cases];t[i]={...t[i],expected:e.target.value};onChange(t)}}/>
+ {!disabled && }
))}
@@ -559,25 +562,28 @@ const QuestionSetsTab = () => {
-

Set Details

-
-
setForm({...form,title:e.target.value})}/>
+
+

Set Details

+ {isPastDeadline && Past Deadline - Only solutions can be updated} +
+
+
setForm({...form,title:e.target.value})}/>
- {['Beginner','Intermediate','Both'].map(lvl=>())} + {['Beginner','Intermediate','Both'].map(lvl=>())}
-
setForm({...form,weekNumber:Number(e.target.value)})}/>
+
setForm({...form,weekNumber:Number(e.target.value)})}/>
-
setForm({...form,deadline:e.target.value})}/>
+
setForm({...form,deadline:e.target.value})}/>

Questions ({form.questions.length}/5)

- {form.questions.length<5&&()} + {form.questions.length<5&&!isPastDeadline&&()}
@@ -589,26 +595,26 @@ const QuestionSetsTab = () => {

{i+1} Question Config

- {form.questions.length>1&&()} + {form.questions.length>1&&!isPastDeadline&&()}
-
updateQuestion(i,'leetcodeSlug',e.target.value)}/>
-
-
updateQuestion(i,'title',e.target.value)}/>
-
-
updateQuestion(i,'points',Number(e.target.value))}/>
-
updateQuestion(i,'category',e.target.value)}/>
-
updateQuestion(i,'orderIndependent',e.target.checked)}/>
-