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,'title',e.target.value)}/>
-
-
updateQuestion(i,'points',Number(e.target.value))}/>
-
updateQuestion(i,'category',e.target.value)}/>
-
updateQuestion(i,'orderIndependent',e.target.checked)}/>
-
-
updateQuestion(i,'hints',v)}/>
+ 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)}/>
+
+ updateQuestion(i,'hints',v)}/>
{renderSolutionEditor({ editorKey: `set-question-${i}`, solutions: q.solutions||[], onChange: v=>updateQuestion(i,'solutions',v) })}
- updateQuestion(i,'functionName',e.target.value.trim())}/>
- updateQuestion(i,'testCases',v)}/>
+ updateQuestion(i,'functionName',e.target.value.trim())}/>
+ updateQuestion(i,'testCases',v)}/>
diff --git a/client/src/pages/ChallengeDetails.jsx b/client/src/pages/ChallengeDetails.jsx
index 7243f7d..bc0f917 100644
--- a/client/src/pages/ChallengeDetails.jsx
+++ b/client/src/pages/ChallengeDetails.jsx
@@ -228,6 +228,7 @@ const ChallengeDetails = () => {
// Logic: LocalStorage Persistence
useEffect(() => {
+ if (isReviewMode) return;
const raw = localStorage.getItem(draftKey);
if (!raw) return;
try {
@@ -240,7 +241,7 @@ const ChallengeDetails = () => {
} catch {
localStorage.removeItem(draftKey);
}
- }, [draftKey]);
+ }, [draftKey, isReviewMode]);
const challengeQuery = useQuery({
queryKey: ["challenge", id],
@@ -259,6 +260,7 @@ const ChallengeDetails = () => {
});
useEffect(() => {
+ if (isReviewMode) return;
const hasAnyCode = Object.values(codeByLang).some((c) => c.trim());
if (!(repoUrl.trim() || hasAnyCode)) {
localStorage.removeItem(draftKey);
@@ -277,7 +279,7 @@ const ChallengeDetails = () => {
updatedAt: new Date().toISOString(),
}),
);
- }, [draftKey, repoUrl, codeByLang, language, challengeQuery.data]);
+ }, [draftKey, repoUrl, codeByLang, language, challengeQuery.data, isReviewMode]);
// A language is offered only if this challenge can actually run in it.
// Python/JS drivers are dynamic; compiled languages need a drivable signature.
diff --git a/client/src/pages/chief/ChiefDashboardTab.jsx b/client/src/pages/chief/ChiefDashboardTab.jsx
index a008280..e5b9124 100644
--- a/client/src/pages/chief/ChiefDashboardTab.jsx
+++ b/client/src/pages/chief/ChiefDashboardTab.jsx
@@ -49,6 +49,7 @@ const ChiefDashboardTab = ({ clan, onTabChange }) => {
const [warningModal, setWarningModal] = useState({ open: false, user: null, message: '' });
const [selectedSetId, setSelectedSetId] = useState(null);
const [hoveredMember, setHoveredMember] = useState(null); // { memberId, name, x, y }
+ const [filterType, setFilterType] = useState('All'); // Filter state
const isArchived = isClanArchived(clan);
const canManageClan = canManageOwnClan(user, clan);
const canArchive = canArchiveClan(user, clan);
@@ -178,6 +179,50 @@ const ChiefDashboardTab = ({ clan, onTabChange }) => {
const circleCircumference = 2 * Math.PI * circleRadius;
const strokeDashoffset = circleCircumference - (completionRate / 100) * circleCircumference;
+ const filteredMembers = members.filter(member => {
+ if (filterType === 'All') return true;
+ const setData = selectedMembers[member._id];
+ const solved = setData?.solved ?? 0;
+ const total = setData?.total ?? (selectedSet?.challengeCount ?? 0);
+ if (filterType === 'Completed') return total > 0 && solved === total;
+ if (filterType === 'In Progress') return solved > 0 && solved < total;
+ if (filterType === 'Not Started') return solved === 0;
+ return true;
+ });
+
+ const handleDownloadCSV = () => {
+ const headers = ['Name', 'Username', 'Email', 'Level', 'XP', 'Solved', 'Total', 'Status'];
+ const rows = filteredMembers.map(member => {
+ const setData = selectedMembers[member._id];
+ const solved = setData?.solved ?? 0;
+ const total = setData?.total ?? (selectedSet?.challengeCount ?? 0);
+ let status = 'Not Started';
+ if (total > 0 && solved === total) status = 'Completed';
+ else if (solved > 0) status = 'In Progress';
+
+ return [
+ `"${member.name || member.username || ''}"`,
+ `"${member.username || ''}"`,
+ `"${member.email || ''}"`,
+ `"${member.codingLevel || 'Beginner'}"`,
+ member.points || 0,
+ solved,
+ total,
+ `"${status}"`
+ ].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', `Clan_Progress_${clan.name?.replace(/\s+/g, '_')}_${selectedSet?.title?.replace(/\s+/g, '_') || 'Overview'}.csv`);
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ };
+
return (
{isArchived && (
@@ -296,10 +341,29 @@ const ChiefDashboardTab = ({ clan, onTabChange }) => {
{/* Right Col: Member Progress List */}
-
+
Member Progress Overview
+
+
+
+
{/* Set selector */}
@@ -332,7 +396,7 @@ const ChiefDashboardTab = ({ clan, onTabChange }) => {
{!setAnalyticsQuery.isLoading && sets.length === 0 && (
No question sets published yet.
)}
- {sets.length > 0 && members.map((member) => {
+ {sets.length > 0 && filteredMembers.map((member) => {
const { text: timeText, isOnline } = getRelativeTime(member.lastLoginDate || member.createdAt);
const isWarned = member.status === 'Warned';
const setData = selectedMembers[member._id];
diff --git a/client/src/pages/chief/ChiefMembersTab.jsx b/client/src/pages/chief/ChiefMembersTab.jsx
index f8199fe..7fc63c1 100644
--- a/client/src/pages/chief/ChiefMembersTab.jsx
+++ b/client/src/pages/chief/ChiefMembersTab.jsx
@@ -19,6 +19,9 @@ const ChiefMembersTab = ({ clan }) => {
const [warnMessage, setWarnMessage] = useState('');
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState('All');
+ const [minSolved, setMinSolved] = useState('');
+ const [maxSolved, setMaxSolved] = useState('');
+ const [sortBy, setSortBy] = useState('Default');
const isArchived = isClanArchived(clan);
const canManageMembers = canManageClanMembers(currentUser, clan);
const canManageClan = canManageOwnClan(currentUser, clan);
@@ -104,29 +107,85 @@ const ChiefMembersTab = ({ clan }) => {
(member.regNo && member.regNo.toLowerCase().includes(searchTerm.toLowerCase())) ||
(member.email && member.email.toLowerCase().includes(searchTerm.toLowerCase()));
const memberStatus = member.status || 'Active';
- if (statusFilter === 'All') return matchesSearch;
- return matchesSearch && memberStatus === statusFilter;
+ const statusMatch = statusFilter === 'All' ? true : memberStatus === statusFilter;
+
+ const solved = member.solvedProblems || 0;
+ const matchesMin = minSolved === '' || solved >= parseInt(minSolved);
+ const matchesMax = maxSolved === '' || solved <= parseInt(maxSolved);
+
+ return matchesSearch && statusMatch && matchesMin && matchesMax;
});
+ const sortedMembers = [...filteredMembers].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', 'Level', 'Solved', 'XP', 'Streak', 'Status'];
+ const rows = sortedMembers.map(user => {
+ const isWarned = user.status === 'Warned';
+ const isInactive = user.status === 'Inactive';
+ const status = isWarned ? 'Warned' : isInactive ? 'Inactive' : 'Active';
+
+ return [
+ `"${user.name || user.username || ''}"`,
+ `"${user.username || ''}"`,
+ `"${user.email || ''}"`,
+ `"${user.regNo || 'N/A'}"`,
+ `"${user.codingLevel || 'Beginner'}"`,
+ user.solvedProblems || 0,
+ user.points || 0,
+ user.streak || 0,
+ `"${status}"`
+ ].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', `Member_Management_${clan.name?.replace(/\s+/g, '_') || 'Clan'}.csv`);
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ };
+
return (
-
-
- Member Management
-
-
-
-
-
setSearchTerm(e.target.value)}
- className="field-input pl-10 h-10 text-sm w-full"
- />
+
+
+
+ Member Management
+
+
+
+
+ setSearchTerm(e.target.value)}
+ className="field-input pl-10 h-10 text-sm w-full"
+ />
+
+
+
+
+
+
@@ -153,7 +242,7 @@ const ChiefMembersTab = ({ clan }) => {
- {filteredMembers.map((user, i) => {
+ {sortedMembers.map((user, i) => {
const isWarned = user.status === 'Warned';
const isInactive = user.status === 'Inactive';
const isActive = !isWarned && !isInactive;
@@ -274,7 +363,7 @@ const ChiefMembersTab = ({ clan }) => {
);
})}
- {filteredMembers.length === 0 && (
+ {sortedMembers.length === 0 && (
|
No members found matching your filters.
diff --git a/server/src/features/clans/clan.controller.js b/server/src/features/clans/clan.controller.js
index e0dbb6c..d4d9d11 100644
--- a/server/src/features/clans/clan.controller.js
+++ b/server/src/features/clans/clan.controller.js
@@ -673,7 +673,7 @@ const updateClan = async (req, res, next) => {
const populated = await Clan.findById(clan._id)
.populate('chief', 'username email')
- .populate('members', 'username email profilePicture points streak')
+ .populate('members', 'name username email profilePicture points streak')
.populate('createdBy', 'username email')
.populate('archivedBy', 'username email')
.populate('restoredBy', 'username email');
@@ -730,7 +730,7 @@ const archiveClan = async (req, res, next) => {
const clan = await Clan.findById(req.params.id)
.populate('chief', 'username email')
- .populate('members', 'username email profilePicture points streak')
+ .populate('members', 'name username email profilePicture points streak')
.populate('createdBy', 'username email')
.populate('archivedBy', 'username email')
.populate('restoredBy', 'username email');
@@ -762,7 +762,7 @@ const restoreClan = async (req, res, next) => {
const populated = await Clan.findById(clan._id)
.populate('chief', 'username email')
- .populate('members', 'username email profilePicture points streak')
+ .populate('members', 'name username email profilePicture points streak')
.populate('createdBy', 'username email')
.populate('archivedBy', 'username email')
.populate('restoredBy', 'username email');
@@ -963,7 +963,7 @@ const assignChief = async (req, res, next) => {
const populated = await Clan.findById(clanId)
.populate('chief', 'username email')
- .populate('members', 'username email profilePicture points streak');
+ .populate('members', 'name username email profilePicture points streak');
sendSuccess(res, { data: populated, message: 'Clan chief assigned' });
@@ -1031,7 +1031,7 @@ const removeChief = async (req, res, next) => {
const populated = await Clan.findById(clanId)
.populate('chief', 'username email')
- .populate('members', 'username email');
+ .populate('members', 'name username email');
return sendSuccess(res, { data: populated, message: 'Clan chief removed' });
} catch (err) {
@@ -1099,7 +1099,7 @@ const addMember = async (req, res, next) => {
const populated = await Clan.findById(clanId)
.populate('chief', 'username email')
- .populate('members', 'username email profilePicture points streak');
+ .populate('members', 'name username email profilePicture points streak');
sendSuccess(res, { data: populated, message: 'Member added' });
@@ -1182,7 +1182,7 @@ const removeMember = async (req, res, next) => {
const populated = await Clan.findById(clanId)
.populate('chief', 'username email')
- .populate('members', 'username email profilePicture points streak');
+ .populate('members', 'name username email profilePicture points streak');
return sendSuccess(res, { data: populated, message: 'Member removed' });
} catch (err) {
|