diff --git a/app/(landing)/organizations/[id]/hackathons/[hackathonId]/judging/page.tsx b/app/(landing)/organizations/[id]/hackathons/[hackathonId]/judging/page.tsx index 530574c1..90425277 100644 --- a/app/(landing)/organizations/[id]/hackathons/[hackathonId]/judging/page.tsx +++ b/app/(landing)/organizations/[id]/hackathons/[hackathonId]/judging/page.tsx @@ -3,6 +3,7 @@ import { useEffect, useState, useCallback } from 'react'; import MetricsCard from '@/components/organization/cards/MetricsCard'; import JudgingParticipant from '@/components/organization/cards/JudgingParticipant'; +import EmptyState from '@/components/EmptyState'; import { useParams } from 'next/navigation'; import { getJudgingSubmissions, @@ -16,6 +17,7 @@ import { type JudgingCriterion, type JudgingSubmission, type JudgingResult, + type AggregatedJudgingResults, } from '@/lib/api/hackathons/judging'; import { getSubmissionDetails } from '@/lib/api/hackathons/participants'; import { getOrganizationMembers } from '@/lib/api/organization'; @@ -49,8 +51,12 @@ export default function JudgingPage() { 'owner' | 'admin' | 'member' | null >(null); const [judgingResults, setJudgingResults] = useState([]); + const [judgingSummary, setJudgingSummary] = + useState(null); const [isFetchingResults, setIsFetchingResults] = useState(false); const [winners, setWinners] = useState([]); + const [winnersSummary, setWinnersSummary] = + useState(null); const [isFetchingWinners, setIsFetchingWinners] = useState(false); const [isPublishing, setIsPublishing] = useState(false); const [isCurrentUserJudge, setIsCurrentUserJudge] = useState(false); @@ -151,16 +157,21 @@ export default function JudgingPage() { setIsFetchingResults(true); try { const res = await getJudgingResults(organizationId, hackathonId); - console.log('Judging Results Response:', res); - if (res.success) { - setJudgingResults(res.data || []); + + if (res.success && res.data) { + setJudgingResults(res.data.results || []); + setJudgingSummary(res.data); } else { setJudgingResults([]); - toast.error(res.message || 'Failed to load judging results'); + setJudgingSummary(null); + if (!res.success) { + toast.error((res as any).message || 'Failed to load judging results'); + } } } catch (error: any) { console.error('Error fetching results:', error); setJudgingResults([]); + setJudgingSummary(null); toast.error( error.response?.data?.message || error.message || @@ -340,8 +351,9 @@ export default function JudgingPage() { setIsFetchingWinners(true); try { const res = await getJudgingWinners(organizationId, hackathonId); - if (res.success) { - setWinners(res.data || []); + if (res.success && res.data) { + setWinners(res.data.results || []); + setWinnersSummary(res.data); } } catch (error) { console.error('Error fetching winners:', error); @@ -368,16 +380,28 @@ export default function JudgingPage() { } }; - // Calculate statistics safely - const gradedCount = judgingResults.length; - const averageHackathonScore = - judgingResults.length > 0 + // Use pre-calculated statistics from the API if available, otherwise fallback to local calculation + const gradedCount = judgingSummary + ? judgingSummary.submissionsScoredCount + : judgingResults.length; + + const totalPossibleSubmissions = judgingSummary + ? judgingSummary.totalSubmissions + : submissions.length; + + const averageHackathonScore = judgingSummary + ? judgingSummary.averageScoreAcrossAll + : judgingResults.length > 0 ? judgingResults.reduce( (acc, curr) => acc + (curr.averageScore || 0), 0 ) / judgingResults.length : 0; + const assignedJudgesCount = judgingSummary + ? judgingSummary.judgesAssigned + : currentJudges.length; + return ( }>
@@ -392,8 +416,8 @@ export default function JudgingPage() {
0 ? Math.round((gradedCount / submissions.length) * 100) : 0}% Completion`} + value={`${gradedCount} / ${totalPossibleSubmissions}`} + subtitle={`${totalPossibleSubmissions > 0 ? Math.round((gradedCount / totalPossibleSubmissions) * 100) : 0}% Completion`} />
@@ -451,7 +475,7 @@ export default function JudgingPage() {
- ) : ( + ) : submissions.length > 0 ? (
{submissions.map(submission => ( ))}
+ ) : ( + )} @@ -489,11 +518,14 @@ export default function JudgingPage() {
{currentJudges.length === 0 ? ( -

- No judges assigned yet. -

+ ) : ( - currentJudges.map((judge: any) => ( + currentJudges.map((judge: any, index: number) => (

- {judge.userId} + Judge {index + 1}

@@ -605,9 +637,12 @@ export default function JudgingPage() { ); })} {orgMembers.length === 0 && !isRefreshingJudges && ( -

- No organization members found. -

+ )}
@@ -648,6 +683,7 @@ export default function JudgingPage() { organizationId={organizationId} hackathonId={hackathonId} totalJudges={currentJudges.length} + criteria={criteria} /> )} @@ -660,12 +696,18 @@ export default function JudgingPage() {
- ) : ( + ) : judgingResults.length > 0 ? ( + ) : ( + )} diff --git a/app/(landing)/organizations/[id]/hackathons/[hackathonId]/submissions/page.tsx b/app/(landing)/organizations/[id]/hackathons/[hackathonId]/submissions/page.tsx index 7e02bb40..f1616cb1 100644 --- a/app/(landing)/organizations/[id]/hackathons/[hackathonId]/submissions/page.tsx +++ b/app/(landing)/organizations/[id]/hackathons/[hackathonId]/submissions/page.tsx @@ -1,13 +1,15 @@ 'use client'; import { useParams } from 'next/navigation'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { Loader2, AlertCircle } from 'lucide-react'; import { useHackathonSubmissions } from '@/hooks/hackathon/use-hackathon-submissions'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { AuthGuard } from '@/components/auth'; import Loading from '@/components/Loading'; import { SubmissionsManagement } from '@/components/organization/hackathons/submissions/SubmissionsManagement'; +import { authClient } from '@/lib/auth-client'; +import { getHackathon, type Hackathon } from '@/lib/api/hackathons'; export default function SubmissionsPage() { const params = useParams(); @@ -26,12 +28,40 @@ export default function SubmissionsPage() { refresh, } = useHackathonSubmissions(hackathonId); + const [currentUserId, setCurrentUserId] = useState(null); + const [hackathon, setHackathon] = useState(null); + useEffect(() => { if (hackathonId) { fetchSubmissions(); + const fetchHackathonDetails = async () => { + try { + const res = await getHackathon(hackathonId); + if (res.success && res.data) { + setHackathon(res.data); + } + } catch (err) { + console.error('Failed to fetch hackathon details:', err); + } + }; + fetchHackathonDetails(); } }, [hackathonId, fetchSubmissions]); + useEffect(() => { + const fetchSession = async () => { + try { + const { data: session } = await authClient.getSession(); + if (session?.user?.id) { + setCurrentUserId(session.user.id); + } + } catch (err) { + console.error('Failed to fetch session:', err); + } + }; + fetchSession(); + }, []); + if (error) { return (
@@ -84,6 +114,8 @@ export default function SubmissionsPage() { onRefresh={refresh} organizationId={organizationId} hackathonId={hackathonId} + currentUserId={currentUserId || undefined} + hackathon={hackathon || undefined} /> )}
diff --git a/components/hackathons/hackathonNavTabs.tsx b/components/hackathons/hackathonNavTabs.tsx index 40dd2a3c..6fa882a5 100644 --- a/components/hackathons/hackathonNavTabs.tsx +++ b/components/hackathons/hackathonNavTabs.tsx @@ -1,4 +1,4 @@ -'use client'; +import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area'; interface HackathonNavTab { id: string; @@ -24,30 +24,33 @@ export function HackathonNavTabs({ return (
-
- {tabs.map(tab => { - const isActive = activeTab === tab.id; - return ( - - ); - })} -
+ + ); + })} +
+ +
); diff --git a/components/organization/cards/GradeSubmissionModal/useScoreForm.ts b/components/organization/cards/GradeSubmissionModal/useScoreForm.ts index 21da726e..0f33b746 100644 --- a/components/organization/cards/GradeSubmissionModal/useScoreForm.ts +++ b/components/organization/cards/GradeSubmissionModal/useScoreForm.ts @@ -170,12 +170,18 @@ export const useScoreForm = ({ setShowSuccess(false); onClose(); }, 2000); + } else { + // Handle API error response + const errorMessage = + response.message || 'Failed to submit grade. Please try again.'; + toast.error(errorMessage); } - } catch (error) { + } catch (error: any) { + // Handle network or unexpected errors const errorMessage = - error instanceof Error - ? error.message - : 'Failed to submit grade. Please try again.'; + error?.response?.data?.message || + error?.message || + 'Failed to submit grade. Please try again.'; toast.error(errorMessage); } finally { setIsLoading(false); diff --git a/components/organization/cards/JudgingParticipant/IndividualScoresBreakdown.tsx b/components/organization/cards/JudgingParticipant/IndividualScoresBreakdown.tsx index d30791d8..f9cd709e 100644 --- a/components/organization/cards/JudgingParticipant/IndividualScoresBreakdown.tsx +++ b/components/organization/cards/JudgingParticipant/IndividualScoresBreakdown.tsx @@ -8,25 +8,65 @@ import { import { Loader2, ChevronDown, ChevronUp, User } from 'lucide-react'; import { cn } from '@/lib/utils'; import { Badge } from '@/components/ui/badge'; +import { + Tooltip, + TooltipTrigger, + TooltipContent, +} from '@/components/ui/tooltip'; interface IndividualScoresBreakdownProps { organizationId: string; hackathonId: string; participantId: string; + initialScores?: Array<{ + judgeId: string; + judgeName: string; + score: number; + }>; +} + +interface JudgeScore { + judgeId: string; + judgeName: string; + totalScore: number; + score?: number; + comment?: string; + criteriaScores?: Array<{ + criterionId: string; + criterionTitle?: string; + score: number; + comment?: string; + }>; } const IndividualScoresBreakdown = ({ organizationId, hackathonId, participantId, + initialScores, }: IndividualScoresBreakdownProps) => { - const [scores, setScores] = useState([]); - const [isLoading, setIsLoading] = useState(true); + const [scores, setScores] = useState([]); + const [isLoading, setIsLoading] = useState(!initialScores); const [expandedJudges, setExpandedJudges] = useState>( {} ); + // Helper to normalize initial scores to JudgeScore shape + const normalizeInitialScores = ( + scores: NonNullable + ): JudgeScore[] => { + return scores.map(s => ({ + judgeId: s.judgeId, + judgeName: s.judgeName, + totalScore: s.score, + score: s.score, + })); + }; + useEffect(() => { + // Always fetch detailed scores to get criteria breakdown + // Even if initialScores are provided, they don't include criteriaScores + // initialScores is only used as a fallback if fetch fails, so we don't need it in deps const fetchScores = async () => { setIsLoading(true); try { @@ -36,16 +76,40 @@ const IndividualScoresBreakdown = ({ participantId ); if (res.success && Array.isArray(res.data)) { - setScores(res.data); + // Map API response to internal state shape + const mappedScores: JudgeScore[] = res.data.map((item: any) => ({ + judgeId: item.judgeId, + judgeName: item.judgeName, + // Ensure totalScore is available, fallback to sum of criteria scores if missing + totalScore: + item.totalScore ?? + item.criteriaScores?.reduce( + (sum: number, c: any) => sum + (c.score || 0), + 0 + ) ?? + 0, + score: item.totalScore, // Keep score for backward compatibility if needed + comment: item.comment, + criteriaScores: item.criteriaScores, + })); + setScores(mappedScores); + } else if (initialScores) { + // Fallback to initialScores if API fails + setScores(normalizeInitialScores(initialScores)); } } catch (err) { console.error('Failed to fetch individual scores:', err); + // Fallback to initialScores if fetch fails + if (initialScores) { + setScores(normalizeInitialScores(initialScores)); + } } finally { setIsLoading(false); } }; fetchScores(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [organizationId, hackathonId, participantId]); const toggleExpand = (judgeId: string) => { @@ -57,7 +121,7 @@ const IndividualScoresBreakdown = ({ const avgTotalScore = scores.length > 0 - ? scores.reduce((sum, s) => sum + s.totalScore, 0) / scores.length + ? scores.reduce((sum, s) => sum + (s.totalScore ?? 0), 0) / scores.length : 0; const getScoreColor = (score: number) => { @@ -91,12 +155,21 @@ const IndividualScoresBreakdown = ({
{scores.some(s => Math.abs(s.totalScore - avgTotalScore) > 2) && ( - - Scoring Discrepancy Detected - + + + + Scoring Discrepancy Detected + + + + One or more judges' scores deviate significantly (>2 + points) from the average. This may indicate differing + interpretations. + + )}
@@ -127,9 +200,18 @@ const IndividualScoresBreakdown = ({ {score.judgeName} {isDiscrepant && ( - - Outlier - + + + + Outlier + + + + This judge's score deviates significantly from the + average, indicating a different perspective on this + submission. + + )}
@@ -137,7 +219,10 @@ const IndividualScoresBreakdown = ({ variant='outline' className='bg-primary/5 text-primary border-primary/20 text-[10px]' > - Total: {score.totalScore.toFixed(1)} + Total:{' '} + {typeof score.totalScore === 'number' + ? score.totalScore.toFixed(1) + : score.score?.toFixed(1) || '0.0'} {expandedJudges[score.judgeId] ? ( @@ -165,34 +250,40 @@ const IndividualScoresBreakdown = ({ -
- {score.criteriaScores.map((c, idx) => ( -
-
- - {c.criterionTitle || c.criterionId}{' '} - - - {c.score} - -
-
-
0 && ( +
+ {score.criteriaScores.map(c => ( +
+
+ + {c.criterionTitle || c.criterionId}{' '} + + + {c.score} + +
+
+
+
+ {c.comment && ( +

+ {c.comment} +

)} - style={{ width: `${c.score * 10}%` }} - /> -
- {c.comment && ( -

- {c.comment} -

- )} +
+ ))}
- ))} -
+ )}
diff --git a/components/organization/hackathons/judging/AggregatedCriteriaBreakdown.tsx b/components/organization/hackathons/judging/AggregatedCriteriaBreakdown.tsx new file mode 100644 index 00000000..9b74ff58 --- /dev/null +++ b/components/organization/hackathons/judging/AggregatedCriteriaBreakdown.tsx @@ -0,0 +1,132 @@ +'use client'; + +import React from 'react'; +import { cn } from '@/lib/utils'; +import { Badge } from '@/components/ui/badge'; +import { + Tooltip, + TooltipTrigger, + TooltipContent, +} from '@/components/ui/tooltip'; +import { Info } from 'lucide-react'; +import { JudgingCriterion } from '@/lib/api/hackathons/judging'; + +interface AggregatedCriteriaBreakdownProps { + criteriaBreakdown: Array<{ + criterionId: string; + averageScore: number; + min: number; + max: number; + variance: number; + }>; + criteria: JudgingCriterion[]; +} + +const AggregatedCriteriaBreakdown = ({ + criteriaBreakdown, + criteria, +}: AggregatedCriteriaBreakdownProps) => { + const getScoreColor = (score: number) => { + if (score >= 8) return 'bg-green-500'; + if (score >= 6) return 'bg-blue-500'; + if (score >= 4) return 'bg-yellow-500'; + return 'bg-red-500'; + }; + + return ( +
+
+
+ Criteria Performance Summary +
+
+ + + +
Range + (Min-Max) + + + + The gap between the highest and lowest scores given by judges for + this criterion + + + + + +
Average + + + + The mean score across all judges for this criterion + + +
+
+ +
+ {criteriaBreakdown.map(item => { + const criterion = criteria.find(c => c.id === item.criterionId); + const title = criterion?.title || criterion?.name || item.criterionId; + + return ( +
+
+ + {title} + +
+ + + + + Var: {(item.variance ?? 0).toFixed(2)} + + + + Variance measures the diversity in judge scoring. Higher + values indicate disagreement among judges. + + + + {(item.averageScore ?? 0).toFixed(2)} + +
+
+ +
+ {/* Min-Max Range Bar */} +
+ {/* Average Marker */} +
+
+ +
+ Min: {(item.min ?? 0).toFixed(1)} + Max: {(item.max ?? 0).toFixed(1)} +
+
+ ); + })} +
+
+ ); +}; + +export default AggregatedCriteriaBreakdown; diff --git a/components/organization/hackathons/judging/JudgingCriteriaList.tsx b/components/organization/hackathons/judging/JudgingCriteriaList.tsx index fefc3628..8c94d2ab 100644 --- a/components/organization/hackathons/judging/JudgingCriteriaList.tsx +++ b/components/organization/hackathons/judging/JudgingCriteriaList.tsx @@ -4,6 +4,7 @@ import React from 'react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Scale, Info } from 'lucide-react'; import type { JudgingCriterion } from '@/lib/api/hackathons'; +import EmptyState from '@/components/EmptyState'; interface JudgingCriteriaListProps { criteria: JudgingCriterion[]; @@ -17,15 +18,10 @@ export function JudgingCriteriaList({ if (!criteria || criteria.length === 0) { return ( emptyState || ( -
- -

- No Judging Criteria Set -

-

- There are no judging criteria defined for this hackathon yet. -

-
+ ) ); } diff --git a/components/organization/hackathons/judging/JudgingResultsTable.tsx b/components/organization/hackathons/judging/JudgingResultsTable.tsx index a5e7bb81..497effd7 100644 --- a/components/organization/hackathons/judging/JudgingResultsTable.tsx +++ b/components/organization/hackathons/judging/JudgingResultsTable.tsx @@ -11,15 +11,23 @@ import { TableRow, } from '@/components/ui/table'; import { Badge } from '@/components/ui/badge'; +import { + Tooltip, + TooltipTrigger, + TooltipContent, +} from '@/components/ui/tooltip'; import { Trophy, Medal, Award, ChevronDown, ChevronUp } from 'lucide-react'; import { cn } from '@/lib/utils'; import IndividualScoresBreakdown from '@/components/organization/cards/JudgingParticipant/IndividualScoresBreakdown'; +import AggregatedCriteriaBreakdown from './AggregatedCriteriaBreakdown'; +import { JudgingCriterion } from '@/lib/api/hackathons/judging'; interface JudgingResultsTableProps { results: JudgingResult[]; organizationId: string; hackathonId: string; totalJudges?: number; + criteria?: JudgingCriterion[]; } // Helper function to safely extract score from JudgingResult @@ -32,6 +40,7 @@ const JudgingResultsTable = ({ organizationId, hackathonId, totalJudges, + criteria = [], }: JudgingResultsTableProps) => { const [expandedRows, setExpandedRows] = React.useState< Record @@ -81,7 +90,9 @@ const JudgingResultsTable = ({ {sortedResults.map((result, index) => { const isFullyGraded = - totalJudges && result.judgeCount >= totalJudges; + result.isComplete || + (result.expectedJudgeCount > 0 && + result.judgeCount >= result.expectedJudgeCount); return ( @@ -92,17 +103,64 @@ const JudgingResultsTable = ({
{getRankIcon(index)} - - # - {result.rank?.position || - (typeof result.rank === 'number' - ? result.rank - : index + 1)} - + #{result.rank ?? index + 1}
- {result.projectName} +
+ {result.projectName} +
+ {result.isPending && ( + + + + Pending + + + + Some judges have scored this submission, but + others are still missing + + + )} + {!result.isComplete && !result.isPending && ( + + + + Incomplete + + + + Not all assigned judges have submitted their + scores yet + + + )} + {result.hasDisagreement && ( + + + + Disagreement + + + + ⚠️ High variance detected: The gap between highest + and lowest judge scores is greater than 3.0. + Review recommended. + + + )} +
+
@@ -129,8 +187,8 @@ const JudgingResultsTable = ({ 'border-green-500/20 bg-green-500/10 text-green-500' )} > - {result.judgeCount} - {totalJudges ? ` / ${totalJudges}` : ''} + {result.judgeCount} /{' '} + {result.expectedJudgeCount ?? totalJudges ?? '?'}
@@ -138,11 +196,22 @@ const JudgingResultsTable = ({ {expandedRows[result.submissionId] && ( -
+
+ {/* Aggregated Criteria Breakdown */} + {result.criteriaBreakdown && + result.criteriaBreakdown.length > 0 && ( + + )} + + {/* Individual Judge Scores */}
diff --git a/components/organization/hackathons/submissions/SubmissionsList.tsx b/components/organization/hackathons/submissions/SubmissionsList.tsx index 46960c3c..5a087045 100644 --- a/components/organization/hackathons/submissions/SubmissionsList.tsx +++ b/components/organization/hackathons/submissions/SubmissionsList.tsx @@ -23,7 +23,7 @@ import { DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { DisqualifyDialog } from './DisqualifyDialog'; -import type { ParticipantSubmission } from '@/lib/api/hackathons'; +import type { ParticipantSubmission, Hackathon } from '@/lib/api/hackathons'; import Image from 'next/image'; interface SubmissionsListProps { @@ -39,6 +39,7 @@ interface SubmissionsListProps { onUpdateRank?: (submissionId: string, rank: number) => Promise; selectedIds?: string[]; onSelectionChange?: (selectedIds: string[]) => void; + hackathon?: Hackathon; } export function SubmissionsList({ @@ -50,6 +51,7 @@ export function SubmissionsList({ onUpdateRank, selectedIds = [], onSelectionChange, + hackathon, }: SubmissionsListProps) { const router = useRouter(); const [reviewingId, setReviewingId] = useState(null); @@ -153,6 +155,10 @@ export function SubmissionsList({ router.push(`/projects/${submissionId}?type=submission`); }; + const isBeforeDeadline = hackathon?.submissionDeadline + ? new Date() < new Date(hackathon.submissionDeadline) + : false; + if (submissions.length === 0 && !loading) { return (
@@ -166,246 +172,36 @@ export function SubmissionsList({ ); } - if (viewMode === 'grid') { - return ( -
- {submissions.map(submission => { - const subData = submission as any; - return ( - handleSubmissionClick(subData.id)} - > - - {/* Selection Checkbox (Absolute positioning) */} - {onSelectionChange && ( -
e.stopPropagation()} - > - toggleSelection(subData.id)} - className='data-[state=checked]:bg-primary data-[state=checked]:border-primary border-gray-500' - /> -
- )} - - {/* Logo, Status and Rank */} -
-
- {subData.logo ? ( - {subData.projectName} - ) : ( -
- {subData.projectName?.charAt(0) || 'P'} -
- )} -
- - {subData.status} - -
- - {/* Project Name */} -

- {subData.projectName || 'Untitled Project'} -

- - {/* Category */} - {subData.category && ( -

- {subData.category} -

- )} - - {/* Type & Date */} -
-
- {subData.participationType === 'TEAM' ? ( - <> - - Team - - ) : ( - <> - - Individual - - )} -
-
- - - {new Date( - subData.submittedAt || subData.createdAt - ).toLocaleDateString()} - -
-
- - {/* Review Actions */} - {onReview && - (subData.status === 'SUBMITTED' || - subData.status === 'SHORTLISTED') && ( -
e.stopPropagation()} - > - {subData.status === 'SUBMITTED' ? ( - - ) : ( - - )} - {onDisqualify && ( - - )} -
- )} - - {/* View Link */} -
- View Details - -
-
-
- ); - })} -
- ); - } - - // Table view return ( -
- - - - {onSelectionChange && ( - - )} - - - - - - - {onReview && ( - - )} - - - + <> + {viewMode === 'grid' ? ( +
{submissions.map(submission => { const subData = submission as any; return ( -
handleSubmissionClick(subData.id)} > - {onSelectionChange && ( - - )} - - - - - - - {onReview && ( - + ); + })} + +
- 0 - } - onCheckedChange={toggleSelectAll} - className='data-[state=checked]:bg-primary data-[state=checked]:border-primary border-gray-500' - /> - - Rank - - Project - - Category - - Type - - Status - - Submitted - - Actions -
e.stopPropagation()} - > - toggleSelection(subData.id)} - className='data-[state=checked]:bg-primary data-[state=checked]:border-primary border-gray-500' - /> - e.stopPropagation()}> - {onUpdateRank ? ( - handleRankUpdate(e, subData.id)} - onBlur={e => handleRankUpdate(e, subData.id)} - /> - ) : ( -
- {subData.rank ? ( -
- - - {subData.rank} - -
- ) : ( - - - )} + + {/* Selection Checkbox (Absolute positioning) */} + {onSelectionChange && ( +
e.stopPropagation()} + > + toggleSelection(subData.id)} + className='data-[state=checked]:bg-primary data-[state=checked]:border-primary border-gray-500' + />
)} -
-
-
+ + {/* Logo, Status and Rank */} +
+
{subData.logo ? ( ) : ( -
+
{subData.projectName?.charAt(0) || 'P'}
)}
- - {subData.projectName || 'Untitled Project'} - + + {subData.status} +
-
- {subData.category || '-'} - -
- {subData.participationType === 'TEAM' ? ( - <> - - Team - - ) : ( - <> - - Individual - - )} + + {/* Project Name */} +

+ {subData.projectName || 'Untitled Project'} +

+ + {/* Category */} + {subData.category && ( +

+ {subData.category} +

+ )} + + {/* Type & Date */} +
+
+ {subData.participationType === 'TEAM' ? ( + <> + + Team + + ) : ( + <> + + Individual + + )} +
+
+ + + {new Date( + subData.submittedAt || subData.createdAt + ).toLocaleDateString()} + +
-
- - {subData.status} - - - {new Date( - subData.submittedAt || subData.createdAt - ).toLocaleDateString()} - e.stopPropagation()}> - {(subData.status === 'SUBMITTED' || + + {/* Review Actions */} + {onReview && + (subData.status === 'SUBMITTED' || subData.status === 'SHORTLISTED') && ( - - +
e.stopPropagation()} + > + {subData.status === 'SUBMITTED' ? ( - - - {subData.status === 'SUBMITTED' ? ( - - handleReview(e, subData.id, 'SHORTLISTED') - } - disabled={reviewingId === subData.id} - className='cursor-pointer text-green-500 focus:bg-green-900/20 focus:text-green-400' - > - - {reviewingId === subData.id - ? 'Approving...' + + {reviewingId === subData.id + ? 'Approving...' + : isBeforeDeadline + ? 'Before Deadline' : 'Approve'} - + + ) : ( + + )} + {onDisqualify && ( + + )} +
+ )} + + {/* View Link */} +
+ View Details + +
+ + + ); + })} + + ) : ( +
+ + + + {onSelectionChange && ( + + )} + + + + + + + {onReview && ( + + )} + + + + {submissions.map(submission => { + const subData = submission as any; + return ( + handleSubmissionClick(subData.id)} + > + {onSelectionChange && ( + + )} + + + + + + + {onReview && ( + )} - - )} - - ); - })} - -
+ 0 + } + onCheckedChange={toggleSelectAll} + className='data-[state=checked]:bg-primary data-[state=checked]:border-primary border-gray-500' + /> + + Rank + + Project + + Category + + Type + + Status + + Submitted + + Actions +
e.stopPropagation()} + > + toggleSelection(subData.id)} + className='data-[state=checked]:bg-primary data-[state=checked]:border-primary border-gray-500' + /> + e.stopPropagation()} + > + {onUpdateRank ? ( + handleRankUpdate(e, subData.id)} + onBlur={e => handleRankUpdate(e, subData.id)} + disabled={true} + /> + ) : ( +
+ {subData.rank ? ( +
+ + + {subData.rank} + +
) : ( - - handleReview(e, subData.id, 'SUBMITTED') - } - disabled={reviewingId === subData.id} - className='cursor-pointer text-yellow-500 focus:bg-yellow-900/20 focus:text-yellow-400' - > - - {reviewingId === subData.id - ? 'Moving...' - : 'Move to Submitted'} - + - )} - {onDisqualify && ( - - handleDisqualifyClick(e, subData.id) - } - className='cursor-pointer text-red-500 focus:bg-red-900/20 focus:text-red-400' - > - - Disqualify - +
+ )} +
+
+
+ {subData.logo ? ( + {subData.projectName} + ) : ( +
+ {subData.projectName?.charAt(0) || 'P'} +
)} - - +
+ + {subData.projectName || 'Untitled Project'} + +
+
+ {subData.category || '-'} + +
+ {subData.participationType === 'TEAM' ? ( + <> + + Team + + ) : ( + <> + + Individual + + )} +
+
+ + {subData.status} + + + {new Date( + subData.submittedAt || subData.createdAt + ).toLocaleDateString()} + e.stopPropagation()} + > + {(subData.status === 'SUBMITTED' || + subData.status === 'SHORTLISTED') && ( + + + + + + {subData.status === 'SUBMITTED' ? ( + + handleReview(e, subData.id, 'SHORTLISTED') + } + disabled={ + reviewingId === subData.id || + isBeforeDeadline + } + className={`cursor-pointer text-green-500 focus:bg-green-900/20 focus:text-green-400 ${isBeforeDeadline ? 'cursor-not-allowed opacity-50' : ''}`} + > + + {reviewingId === subData.id + ? 'Approving...' + : isBeforeDeadline + ? 'Before Deadline' + : 'Approve'} + + ) : ( + + handleReview(e, subData.id, 'SUBMITTED') + } + disabled={reviewingId === subData.id} + className='cursor-pointer text-yellow-500 focus:bg-yellow-900/20 focus:text-yellow-400' + > + + {reviewingId === subData.id + ? 'Moving...' + : 'Move to Submitted'} + + )} + {onDisqualify && ( + + handleDisqualifyClick(e, subData.id) + } + className='cursor-pointer text-red-500 focus:bg-red-900/20 focus:text-red-400' + > + + Disqualify + + )} + + + )} +
+
+
+ )} {onDisqualify && ( )} -
+ ); } diff --git a/components/organization/hackathons/submissions/SubmissionsManagement.tsx b/components/organization/hackathons/submissions/SubmissionsManagement.tsx index 522ddc9e..b202a44d 100644 --- a/components/organization/hackathons/submissions/SubmissionsManagement.tsx +++ b/components/organization/hackathons/submissions/SubmissionsManagement.tsx @@ -22,7 +22,7 @@ import { import { Button } from '@/components/ui/button'; import { SubmissionsList } from './SubmissionsList'; import { DisqualifyDialog } from './DisqualifyDialog'; -import type { ParticipantSubmission } from '@/lib/api/hackathons'; +import type { ParticipantSubmission, Hackathon } from '@/lib/api/hackathons'; import { useReviewSubmission } from '@/hooks/hackathon/use-review-submission'; import { useDisqualifySubmission } from '@/hooks/hackathon/use-disqualify-submission'; import { useBulkAction } from '@/hooks/hackathon/use-bulk-action'; @@ -61,6 +61,8 @@ interface SubmissionsManagementProps { onRefresh: () => void; organizationId?: string; hackathonId?: string; + currentUserId?: string; + hackathon?: Hackathon; } /* -------------------------------------------------------------------------- */ @@ -77,6 +79,8 @@ export function SubmissionsManagement({ onRefresh, organizationId, hackathonId, + currentUserId, + hackathon, }: SubmissionsManagementProps) { const [viewMode, setViewMode] = useState<'grid' | 'table'>('grid'); const [searchTerm, setSearchTerm] = useState(filters.search ?? ''); @@ -94,15 +98,19 @@ export function SubmissionsManagement({ submissionId: string, status: 'SHORTLISTED' | 'SUBMITTED' ) => { - if (!organizationId || !hackathonId) return; - await review(organizationId, hackathonId, submissionId, { status }); + if (!organizationId || !hackathonId || !currentUserId) return; + await review(organizationId, hackathonId, submissionId, { + status, + judgeId: currentUserId, + }); onRefresh(); }; const handleDisqualify = async (submissionId: string, reason: string) => { - if (!organizationId || !hackathonId) return; + if (!organizationId || !hackathonId || !currentUserId) return; await disqualify(organizationId, hackathonId, submissionId, { disqualificationReason: reason, + judgeId: currentUserId, }); onRefresh(); }; @@ -127,6 +135,7 @@ export function SubmissionsManagement({ await performBulkAction(organizationId, hackathonId, { submissionIds: selectedIds, action, + judgeId: currentUserId || '', reason, }); @@ -312,6 +321,7 @@ export function SubmissionsManagement({ } selectedIds={selectedIds} onSelectionChange={setSelectedIds} + hackathon={hackathon} /> {/* Bulk Disqualify Dialog */} diff --git a/components/ui/shadcn-io/announcement-editor/index.tsx b/components/ui/shadcn-io/announcement-editor/index.tsx index 209ed2b5..d29c90eb 100644 --- a/components/ui/shadcn-io/announcement-editor/index.tsx +++ b/components/ui/shadcn-io/announcement-editor/index.tsx @@ -363,7 +363,7 @@ function AnnouncementEditor({
-