Observer platform: mobile fixes, data/UX overhaul, animated nav
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m41s
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m41s
- Fix dashboard default round selection to target active round instead of R1 - Move edition selector from dashboard header to hamburger menu via shared context - Add observer-friendly status labels (Not Reviewed / Under Review / Reviewed) - Fix pipeline completion: closed rounds show 100%, cap all rates at 100% - Round badge on projects list shows furthest round reached - Hide scores/evals for projects with zero evaluations - Enhance project detail round history with pass/reject indicators from ProjectRoundState - Remove irrelevant fields (Org Type, Budget, Duration) from project detail - Clickable juror workload with expandable project assignments - Humanize activity feed with icons and readable messages - Fix jurors table: responsive card layout on mobile - Fix criteria chart: horizontal bars for readable labels on mobile - Animate hamburger menu open/close with CSS grid transition Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -123,6 +123,7 @@ export const analyticsRouter = router({
|
||||
where: assignmentWhere(input),
|
||||
include: {
|
||||
user: { select: { name: true } },
|
||||
project: { select: { id: true, title: true } },
|
||||
evaluation: {
|
||||
select: { id: true, status: true },
|
||||
},
|
||||
@@ -132,7 +133,7 @@ export const analyticsRouter = router({
|
||||
// Group by user
|
||||
const byUser: Record<
|
||||
string,
|
||||
{ name: string; assigned: number; completed: number }
|
||||
{ name: string; assigned: number; completed: number; projects: { id: string; title: string; evalStatus: string }[] }
|
||||
> = {}
|
||||
|
||||
assignments.forEach((assignment) => {
|
||||
@@ -142,12 +143,19 @@ export const analyticsRouter = router({
|
||||
name: assignment.user.name || 'Unknown',
|
||||
assigned: 0,
|
||||
completed: 0,
|
||||
projects: [],
|
||||
}
|
||||
}
|
||||
byUser[userId].assigned++
|
||||
if (assignment.evaluation?.status === 'SUBMITTED') {
|
||||
const evalStatus = assignment.evaluation?.status
|
||||
if (evalStatus === 'SUBMITTED') {
|
||||
byUser[userId].completed++
|
||||
}
|
||||
byUser[userId].projects.push({
|
||||
id: assignment.project.id,
|
||||
title: assignment.project.title,
|
||||
evalStatus: evalStatus === 'SUBMITTED' ? 'REVIEWED' : evalStatus === 'DRAFT' ? 'UNDER_REVIEW' : 'NOT_REVIEWED',
|
||||
})
|
||||
})
|
||||
|
||||
return Object.entries(byUser)
|
||||
@@ -676,7 +684,7 @@ export const analyticsRouter = router({
|
||||
])
|
||||
|
||||
const completionRate = totalAssignments > 0
|
||||
? Math.round((submittedEvaluations / totalAssignments) * 100)
|
||||
? Math.min(100, Math.round((submittedEvaluations / totalAssignments) * 100))
|
||||
: 0
|
||||
|
||||
const scores = evaluationScores.map((e) => e.globalScore!).filter((s) => s != null)
|
||||
@@ -850,9 +858,11 @@ export const analyticsRouter = router({
|
||||
const totalProjects = stateBreakdown.reduce((sum, ps) => sum + ps.count, 0)
|
||||
const totalAssignments = assignmentCountByRound.get(round.id) || 0
|
||||
const completedEvaluations = completedEvalsByRound.get(round.id) || 0
|
||||
const completionRate = totalAssignments > 0
|
||||
? Math.round((completedEvaluations / totalAssignments) * 100)
|
||||
: 0
|
||||
const completionRate = (round.status === 'ROUND_CLOSED' || round.status === 'ROUND_ARCHIVED')
|
||||
? 100
|
||||
: totalAssignments > 0
|
||||
? Math.min(100, Math.round((completedEvaluations / totalAssignments) * 100))
|
||||
: 0
|
||||
|
||||
return {
|
||||
roundId: round.id,
|
||||
@@ -914,7 +924,8 @@ export const analyticsRouter = router({
|
||||
where.projectRoundStates = { some: { roundId: input.roundId } }
|
||||
}
|
||||
|
||||
if (input.status) {
|
||||
const OBSERVER_DERIVED_STATUSES = ['NOT_REVIEWED', 'UNDER_REVIEW', 'REVIEWED']
|
||||
if (input.status && !OBSERVER_DERIVED_STATUSES.includes(input.status)) {
|
||||
where.status = input.status
|
||||
}
|
||||
|
||||
@@ -942,16 +953,25 @@ export const analyticsRouter = router({
|
||||
assignments: {
|
||||
select: {
|
||||
roundId: true,
|
||||
round: { select: { id: true, name: true } },
|
||||
round: { select: { id: true, name: true, sortOrder: true } },
|
||||
evaluation: {
|
||||
select: { globalScore: true, status: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
projectRoundStates: {
|
||||
select: {
|
||||
roundId: true,
|
||||
state: true,
|
||||
round: { select: { id: true, name: true, sortOrder: true } },
|
||||
},
|
||||
orderBy: { round: { sortOrder: 'desc' } },
|
||||
take: 1,
|
||||
},
|
||||
},
|
||||
orderBy: prismaOrderBy,
|
||||
// When sorting by computed fields, fetch all then slice in JS
|
||||
...(input.sortBy === 'title'
|
||||
// When sorting by computed fields or filtering by observer-derived status, fetch all then slice in JS
|
||||
...(input.sortBy === 'title' && !OBSERVER_DERIVED_STATUSES.includes(input.status ?? '')
|
||||
? { skip: (input.page - 1) * input.perPage, take: input.perPage }
|
||||
: {}),
|
||||
}),
|
||||
@@ -962,6 +982,9 @@ export const analyticsRouter = router({
|
||||
const submitted = p.assignments
|
||||
.map((a) => a.evaluation)
|
||||
.filter((e) => e?.status === 'SUBMITTED')
|
||||
const drafts = p.assignments
|
||||
.map((a) => a.evaluation)
|
||||
.filter((e) => e?.status === 'DRAFT')
|
||||
const scores = submitted
|
||||
.map((e) => e?.globalScore)
|
||||
.filter((s): s is number => s !== null)
|
||||
@@ -970,51 +993,74 @@ export const analyticsRouter = router({
|
||||
? scores.reduce((a, b) => a + b, 0) / scores.length
|
||||
: null
|
||||
|
||||
// Filter assignments to the queried round so we show the correct round name
|
||||
// Show the furthest round the project reached (from projectRoundStates, ordered by sortOrder desc)
|
||||
const furthestRoundState = p.projectRoundStates[0]
|
||||
// Fallback to assignment round if no round states
|
||||
const roundAssignment = input.roundId
|
||||
? p.assignments.find((a) => a.roundId === input.roundId)
|
||||
: p.assignments[0]
|
||||
|
||||
// Derive observer-friendly status
|
||||
let observerStatus: string
|
||||
if (p.status === 'REJECTED') observerStatus = 'REJECTED'
|
||||
else if (p.status === 'SEMIFINALIST') observerStatus = 'SEMIFINALIST'
|
||||
else if (p.status === 'FINALIST') observerStatus = 'FINALIST'
|
||||
else if (p.status === 'SUBMITTED') observerStatus = 'SUBMITTED'
|
||||
else if (submitted.length > 0) observerStatus = 'REVIEWED'
|
||||
else if (drafts.length > 0) observerStatus = 'UNDER_REVIEW'
|
||||
else observerStatus = 'NOT_REVIEWED'
|
||||
|
||||
return {
|
||||
id: p.id,
|
||||
title: p.title,
|
||||
teamName: p.teamName,
|
||||
status: p.status,
|
||||
observerStatus,
|
||||
country: p.country,
|
||||
roundId: roundAssignment?.round?.id ?? '',
|
||||
roundName: roundAssignment?.round?.name ?? '',
|
||||
roundId: furthestRoundState?.round?.id ?? roundAssignment?.round?.id ?? '',
|
||||
roundName: furthestRoundState?.round?.name ?? roundAssignment?.round?.name ?? '',
|
||||
averageScore,
|
||||
evaluationCount: submitted.length,
|
||||
}
|
||||
})
|
||||
|
||||
// Filter by observer-derived status in JS
|
||||
const observerStatusFilter = input.status && OBSERVER_DERIVED_STATUSES.includes(input.status)
|
||||
? input.status
|
||||
: null
|
||||
const filtered = observerStatusFilter
|
||||
? mapped.filter((p) => p.observerStatus === observerStatusFilter)
|
||||
: mapped
|
||||
const filteredTotal = observerStatusFilter ? filtered.length : total
|
||||
|
||||
// Sort by computed fields (score, evaluations) in JS
|
||||
let sorted = mapped
|
||||
let sorted = filtered
|
||||
if (input.sortBy === 'score') {
|
||||
sorted = mapped.sort((a, b) => {
|
||||
sorted = filtered.sort((a, b) => {
|
||||
const sa = a.averageScore ?? -1
|
||||
const sb = b.averageScore ?? -1
|
||||
return input.sortDir === 'asc' ? sa - sb : sb - sa
|
||||
})
|
||||
} else if (input.sortBy === 'evaluations') {
|
||||
sorted = mapped.sort((a, b) =>
|
||||
sorted = filtered.sort((a, b) =>
|
||||
input.sortDir === 'asc'
|
||||
? a.evaluationCount - b.evaluationCount
|
||||
: b.evaluationCount - a.evaluationCount
|
||||
)
|
||||
}
|
||||
|
||||
// Paginate in JS for computed-field sorts
|
||||
const paginated = input.sortBy !== 'title'
|
||||
// Paginate in JS for computed-field sorts or observer status filter
|
||||
const needsJsPagination = input.sortBy !== 'title' || observerStatusFilter
|
||||
const paginated = needsJsPagination
|
||||
? sorted.slice((input.page - 1) * input.perPage, input.page * input.perPage)
|
||||
: sorted
|
||||
|
||||
return {
|
||||
projects: paginated,
|
||||
total,
|
||||
total: filteredTotal,
|
||||
page: input.page,
|
||||
perPage: input.perPage,
|
||||
totalPages: Math.ceil(total / input.perPage),
|
||||
totalPages: Math.ceil(filteredTotal / input.perPage),
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -1256,15 +1302,21 @@ export const analyticsRouter = router({
|
||||
}
|
||||
|
||||
// Get competition rounds for file grouping
|
||||
let competitionRounds: { id: string; name: string }[] = []
|
||||
let competitionRounds: { id: string; name: string; roundType: string }[] = []
|
||||
const competition = await ctx.prisma.competition.findFirst({
|
||||
where: { programId: projectRaw.programId },
|
||||
include: { rounds: { select: { id: true, name: true }, orderBy: { sortOrder: 'asc' } } },
|
||||
include: { rounds: { select: { id: true, name: true, roundType: true }, orderBy: { sortOrder: 'asc' } } },
|
||||
})
|
||||
if (competition) {
|
||||
competitionRounds = competition.rounds
|
||||
}
|
||||
|
||||
// Get project round states for round history
|
||||
const projectRoundStates = await ctx.prisma.projectRoundState.findMany({
|
||||
where: { projectId: input.id },
|
||||
select: { roundId: true, state: true, enteredAt: true, exitedAt: true },
|
||||
})
|
||||
|
||||
// Get file requirements for all rounds
|
||||
let allRequirements: { id: string; roundId: string; name: string; description: string | null; isRequired: boolean; acceptedMimeTypes: string[]; maxSizeMB: number | null }[] = []
|
||||
if (competitionRounds.length > 0) {
|
||||
@@ -1306,6 +1358,7 @@ export const analyticsRouter = router({
|
||||
assignments: assignmentsWithAvatars,
|
||||
stats,
|
||||
competitionRounds,
|
||||
projectRoundStates,
|
||||
allRequirements,
|
||||
}
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user