2026-01-30 13:41:32 +01:00
|
|
|
import { z } from 'zod'
|
|
|
|
|
import { TRPCError } from '@trpc/server'
|
Implement 15 platform features: digest, availability, templates, comparison, live voting SSE, file versioning, mentorship, messaging, analytics, drafts, webhooks, peer review, audit enhancements, i18n
Features implemented:
- F1: Email digest notifications with cron endpoint and per-user frequency
- F2: Jury availability windows and workload preferences in smart assignment
- F3: Round templates with save-from-round and CRUD management
- F4: Side-by-side project comparison view for jury members
- F5: Real-time voting dashboard with Server-Sent Events (SSE)
- F6: Live voting UX: QR codes, audience voting, tie-breaking, score animations
- F7: File versioning, inline preview, bulk download with presigned URLs
- F8: Mentor dashboard: milestones, private notes, activity tracking
- F9: Communication hub with broadcasts, templates, and recipient targeting
- F10: Advanced analytics: cross-round comparison, juror consistency, diversity metrics, PDF export
- F11: Applicant draft saving with magic link resume and cron cleanup
- F12: Webhook integration layer with HMAC signing, retry, and delivery logs
- F13: Peer review discussions with anonymized scores and threaded comments
- F14: Audit log enhancements: before/after diffs, session grouping, anomaly detection, retention
- F15: i18n foundation with next-intl (EN/FR), cookie-based locale, language switcher
Schema: 12 new models, field additions to User, Project, ProjectFile, LiveVotingSession, LiveVote, MentorAssignment, AuditLog, Program
New routers: roundTemplate, message, webhook (registered in _app.ts)
New services: email-digest, webhook-dispatcher
New cron endpoints: /api/cron/digest, /api/cron/draft-cleanup, /api/cron/audit-cleanup
New API routes: /api/live-voting/stream (SSE), /api/files/bulk-download
All features are admin-configurable via SystemSettings or per-model settingsJson fields.
Docker build verified successfully.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-05 23:31:41 +01:00
|
|
|
import { router, protectedProcedure, adminProcedure, publicProcedure } from '../trpc'
|
2026-02-05 21:09:06 +01:00
|
|
|
import { logAudit } from '../utils/audit'
|
2026-01-30 13:41:32 +01:00
|
|
|
|
|
|
|
|
export const liveVotingRouter = router({
|
|
|
|
|
/**
|
|
|
|
|
* Get or create a live voting session for a round
|
|
|
|
|
*/
|
|
|
|
|
getSession: adminProcedure
|
|
|
|
|
.input(z.object({ roundId: z.string() }))
|
|
|
|
|
.query(async ({ ctx, input }) => {
|
|
|
|
|
let session = await ctx.prisma.liveVotingSession.findUnique({
|
|
|
|
|
where: { roundId: input.roundId },
|
|
|
|
|
include: {
|
|
|
|
|
round: {
|
|
|
|
|
include: {
|
|
|
|
|
program: { select: { name: true, year: true } },
|
2026-02-04 14:15:06 +01:00
|
|
|
projects: {
|
2026-01-30 13:41:32 +01:00
|
|
|
where: { status: { in: ['FINALIST', 'SEMIFINALIST'] } },
|
2026-02-04 14:15:06 +01:00
|
|
|
select: { id: true, title: true, teamName: true },
|
2026-01-30 13:41:32 +01:00
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (!session) {
|
|
|
|
|
// Create session
|
|
|
|
|
session = await ctx.prisma.liveVotingSession.create({
|
|
|
|
|
data: {
|
|
|
|
|
roundId: input.roundId,
|
|
|
|
|
},
|
|
|
|
|
include: {
|
|
|
|
|
round: {
|
|
|
|
|
include: {
|
|
|
|
|
program: { select: { name: true, year: true } },
|
2026-02-04 14:15:06 +01:00
|
|
|
projects: {
|
2026-01-30 13:41:32 +01:00
|
|
|
where: { status: { in: ['FINALIST', 'SEMIFINALIST'] } },
|
2026-02-04 14:15:06 +01:00
|
|
|
select: { id: true, title: true, teamName: true },
|
2026-01-30 13:41:32 +01:00
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get current votes if voting is in progress
|
|
|
|
|
let currentVotes: { userId: string; score: number }[] = []
|
|
|
|
|
if (session.currentProjectId) {
|
|
|
|
|
const votes = await ctx.prisma.liveVote.findMany({
|
|
|
|
|
where: {
|
|
|
|
|
sessionId: session.id,
|
|
|
|
|
projectId: session.currentProjectId,
|
|
|
|
|
},
|
|
|
|
|
select: { userId: true, score: true },
|
|
|
|
|
})
|
|
|
|
|
currentVotes = votes
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...session,
|
|
|
|
|
currentVotes,
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get session for jury member voting
|
|
|
|
|
*/
|
|
|
|
|
getSessionForVoting: protectedProcedure
|
|
|
|
|
.input(z.object({ sessionId: z.string() }))
|
|
|
|
|
.query(async ({ ctx, input }) => {
|
|
|
|
|
const session = await ctx.prisma.liveVotingSession.findUniqueOrThrow({
|
|
|
|
|
where: { id: input.sessionId },
|
|
|
|
|
include: {
|
|
|
|
|
round: {
|
|
|
|
|
include: {
|
|
|
|
|
program: { select: { name: true, year: true } },
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Get current project if in progress
|
|
|
|
|
let currentProject = null
|
|
|
|
|
if (session.currentProjectId && session.status === 'IN_PROGRESS') {
|
|
|
|
|
currentProject = await ctx.prisma.project.findUnique({
|
|
|
|
|
where: { id: session.currentProjectId },
|
|
|
|
|
select: { id: true, title: true, teamName: true, description: true },
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get user's vote for current project
|
|
|
|
|
let userVote = null
|
|
|
|
|
if (session.currentProjectId) {
|
|
|
|
|
userVote = await ctx.prisma.liveVote.findFirst({
|
|
|
|
|
where: {
|
|
|
|
|
sessionId: session.id,
|
|
|
|
|
projectId: session.currentProjectId,
|
|
|
|
|
userId: ctx.user.id,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Calculate time remaining
|
|
|
|
|
let timeRemaining = null
|
|
|
|
|
if (session.votingEndsAt && session.status === 'IN_PROGRESS') {
|
|
|
|
|
const remaining = new Date(session.votingEndsAt).getTime() - Date.now()
|
|
|
|
|
timeRemaining = Math.max(0, Math.floor(remaining / 1000))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
session: {
|
|
|
|
|
id: session.id,
|
|
|
|
|
status: session.status,
|
|
|
|
|
votingStartedAt: session.votingStartedAt,
|
|
|
|
|
votingEndsAt: session.votingEndsAt,
|
|
|
|
|
},
|
|
|
|
|
round: session.round,
|
|
|
|
|
currentProject,
|
|
|
|
|
userVote,
|
|
|
|
|
timeRemaining,
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get public session info for display
|
|
|
|
|
*/
|
|
|
|
|
getPublicSession: protectedProcedure
|
|
|
|
|
.input(z.object({ sessionId: z.string() }))
|
|
|
|
|
.query(async ({ ctx, input }) => {
|
|
|
|
|
const session = await ctx.prisma.liveVotingSession.findUniqueOrThrow({
|
|
|
|
|
where: { id: input.sessionId },
|
|
|
|
|
include: {
|
|
|
|
|
round: {
|
|
|
|
|
include: {
|
|
|
|
|
program: { select: { name: true, year: true } },
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Get all projects in order
|
|
|
|
|
const projectOrder = (session.projectOrderJson as string[]) || []
|
|
|
|
|
const projects = await ctx.prisma.project.findMany({
|
|
|
|
|
where: { id: { in: projectOrder } },
|
|
|
|
|
select: { id: true, title: true, teamName: true },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Sort by order
|
|
|
|
|
const sortedProjects = projectOrder
|
|
|
|
|
.map((id) => projects.find((p) => p.id === id))
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
|
|
|
|
|
// Get scores for each project
|
|
|
|
|
const scores = await ctx.prisma.liveVote.groupBy({
|
|
|
|
|
by: ['projectId'],
|
|
|
|
|
where: { sessionId: session.id },
|
|
|
|
|
_avg: { score: true },
|
|
|
|
|
_count: true,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const projectsWithScores = sortedProjects.map((project) => {
|
|
|
|
|
const projectScore = scores.find((s) => s.projectId === project!.id)
|
|
|
|
|
return {
|
|
|
|
|
...project,
|
|
|
|
|
averageScore: projectScore?._avg.score || null,
|
|
|
|
|
voteCount: projectScore?._count || 0,
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
session: {
|
|
|
|
|
id: session.id,
|
|
|
|
|
status: session.status,
|
|
|
|
|
currentProjectId: session.currentProjectId,
|
|
|
|
|
votingEndsAt: session.votingEndsAt,
|
|
|
|
|
},
|
|
|
|
|
round: session.round,
|
|
|
|
|
projects: projectsWithScores,
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Set project order for voting
|
|
|
|
|
*/
|
|
|
|
|
setProjectOrder: adminProcedure
|
|
|
|
|
.input(
|
|
|
|
|
z.object({
|
|
|
|
|
sessionId: z.string(),
|
|
|
|
|
projectIds: z.array(z.string()),
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
.mutation(async ({ ctx, input }) => {
|
|
|
|
|
const session = await ctx.prisma.liveVotingSession.update({
|
|
|
|
|
where: { id: input.sessionId },
|
|
|
|
|
data: {
|
|
|
|
|
projectOrderJson: input.projectIds,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return session
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Start voting for a project
|
|
|
|
|
*/
|
|
|
|
|
startVoting: adminProcedure
|
|
|
|
|
.input(
|
|
|
|
|
z.object({
|
|
|
|
|
sessionId: z.string(),
|
|
|
|
|
projectId: z.string(),
|
|
|
|
|
durationSeconds: z.number().int().min(10).max(300).default(30),
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
.mutation(async ({ ctx, input }) => {
|
|
|
|
|
const now = new Date()
|
|
|
|
|
const votingEndsAt = new Date(now.getTime() + input.durationSeconds * 1000)
|
|
|
|
|
|
|
|
|
|
const session = await ctx.prisma.liveVotingSession.update({
|
|
|
|
|
where: { id: input.sessionId },
|
|
|
|
|
data: {
|
|
|
|
|
status: 'IN_PROGRESS',
|
|
|
|
|
currentProjectId: input.projectId,
|
|
|
|
|
votingStartedAt: now,
|
|
|
|
|
votingEndsAt,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Audit log
|
2026-02-05 21:09:06 +01:00
|
|
|
await logAudit({
|
|
|
|
|
prisma: ctx.prisma,
|
|
|
|
|
userId: ctx.user.id,
|
|
|
|
|
action: 'START_VOTING',
|
|
|
|
|
entityType: 'LiveVotingSession',
|
|
|
|
|
entityId: session.id,
|
|
|
|
|
detailsJson: { projectId: input.projectId, durationSeconds: input.durationSeconds },
|
|
|
|
|
ipAddress: ctx.ip,
|
|
|
|
|
userAgent: ctx.userAgent,
|
2026-01-30 13:41:32 +01:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return session
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Stop voting
|
|
|
|
|
*/
|
|
|
|
|
stopVoting: adminProcedure
|
|
|
|
|
.input(z.object({ sessionId: z.string() }))
|
|
|
|
|
.mutation(async ({ ctx, input }) => {
|
|
|
|
|
const session = await ctx.prisma.liveVotingSession.update({
|
|
|
|
|
where: { id: input.sessionId },
|
|
|
|
|
data: {
|
|
|
|
|
status: 'PAUSED',
|
|
|
|
|
votingEndsAt: new Date(),
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return session
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* End session
|
|
|
|
|
*/
|
|
|
|
|
endSession: adminProcedure
|
|
|
|
|
.input(z.object({ sessionId: z.string() }))
|
|
|
|
|
.mutation(async ({ ctx, input }) => {
|
|
|
|
|
const session = await ctx.prisma.liveVotingSession.update({
|
|
|
|
|
where: { id: input.sessionId },
|
|
|
|
|
data: {
|
|
|
|
|
status: 'COMPLETED',
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Audit log
|
2026-02-05 21:09:06 +01:00
|
|
|
await logAudit({
|
|
|
|
|
prisma: ctx.prisma,
|
|
|
|
|
userId: ctx.user.id,
|
|
|
|
|
action: 'END_SESSION',
|
|
|
|
|
entityType: 'LiveVotingSession',
|
|
|
|
|
entityId: session.id,
|
|
|
|
|
detailsJson: {},
|
|
|
|
|
ipAddress: ctx.ip,
|
|
|
|
|
userAgent: ctx.userAgent,
|
2026-01-30 13:41:32 +01:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return session
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Submit a vote
|
|
|
|
|
*/
|
|
|
|
|
vote: protectedProcedure
|
|
|
|
|
.input(
|
|
|
|
|
z.object({
|
|
|
|
|
sessionId: z.string(),
|
|
|
|
|
projectId: z.string(),
|
|
|
|
|
score: z.number().int().min(1).max(10),
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
.mutation(async ({ ctx, input }) => {
|
|
|
|
|
// Verify session is in progress
|
|
|
|
|
const session = await ctx.prisma.liveVotingSession.findUniqueOrThrow({
|
|
|
|
|
where: { id: input.sessionId },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (session.status !== 'IN_PROGRESS') {
|
|
|
|
|
throw new TRPCError({
|
|
|
|
|
code: 'BAD_REQUEST',
|
|
|
|
|
message: 'Voting is not currently active',
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (session.currentProjectId !== input.projectId) {
|
|
|
|
|
throw new TRPCError({
|
|
|
|
|
code: 'BAD_REQUEST',
|
|
|
|
|
message: 'Cannot vote for this project right now',
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if voting window is still open
|
|
|
|
|
if (session.votingEndsAt && new Date() > session.votingEndsAt) {
|
|
|
|
|
throw new TRPCError({
|
|
|
|
|
code: 'BAD_REQUEST',
|
|
|
|
|
message: 'Voting window has closed',
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Upsert vote (allow vote change during window)
|
|
|
|
|
const vote = await ctx.prisma.liveVote.upsert({
|
|
|
|
|
where: {
|
|
|
|
|
sessionId_projectId_userId: {
|
|
|
|
|
sessionId: input.sessionId,
|
|
|
|
|
projectId: input.projectId,
|
|
|
|
|
userId: ctx.user.id,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
create: {
|
|
|
|
|
sessionId: input.sessionId,
|
|
|
|
|
projectId: input.projectId,
|
|
|
|
|
userId: ctx.user.id,
|
|
|
|
|
score: input.score,
|
|
|
|
|
},
|
|
|
|
|
update: {
|
|
|
|
|
score: input.score,
|
|
|
|
|
votedAt: new Date(),
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return vote
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
Implement 15 platform features: digest, availability, templates, comparison, live voting SSE, file versioning, mentorship, messaging, analytics, drafts, webhooks, peer review, audit enhancements, i18n
Features implemented:
- F1: Email digest notifications with cron endpoint and per-user frequency
- F2: Jury availability windows and workload preferences in smart assignment
- F3: Round templates with save-from-round and CRUD management
- F4: Side-by-side project comparison view for jury members
- F5: Real-time voting dashboard with Server-Sent Events (SSE)
- F6: Live voting UX: QR codes, audience voting, tie-breaking, score animations
- F7: File versioning, inline preview, bulk download with presigned URLs
- F8: Mentor dashboard: milestones, private notes, activity tracking
- F9: Communication hub with broadcasts, templates, and recipient targeting
- F10: Advanced analytics: cross-round comparison, juror consistency, diversity metrics, PDF export
- F11: Applicant draft saving with magic link resume and cron cleanup
- F12: Webhook integration layer with HMAC signing, retry, and delivery logs
- F13: Peer review discussions with anonymized scores and threaded comments
- F14: Audit log enhancements: before/after diffs, session grouping, anomaly detection, retention
- F15: i18n foundation with next-intl (EN/FR), cookie-based locale, language switcher
Schema: 12 new models, field additions to User, Project, ProjectFile, LiveVotingSession, LiveVote, MentorAssignment, AuditLog, Program
New routers: roundTemplate, message, webhook (registered in _app.ts)
New services: email-digest, webhook-dispatcher
New cron endpoints: /api/cron/digest, /api/cron/draft-cleanup, /api/cron/audit-cleanup
New API routes: /api/live-voting/stream (SSE), /api/files/bulk-download
All features are admin-configurable via SystemSettings or per-model settingsJson fields.
Docker build verified successfully.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-05 23:31:41 +01:00
|
|
|
* Get results for a session (with weighted jury + audience scoring)
|
2026-01-30 13:41:32 +01:00
|
|
|
*/
|
|
|
|
|
getResults: protectedProcedure
|
|
|
|
|
.input(z.object({ sessionId: z.string() }))
|
|
|
|
|
.query(async ({ ctx, input }) => {
|
|
|
|
|
const session = await ctx.prisma.liveVotingSession.findUniqueOrThrow({
|
|
|
|
|
where: { id: input.sessionId },
|
|
|
|
|
include: {
|
|
|
|
|
round: {
|
|
|
|
|
include: {
|
|
|
|
|
program: { select: { name: true, year: true } },
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
Implement 15 platform features: digest, availability, templates, comparison, live voting SSE, file versioning, mentorship, messaging, analytics, drafts, webhooks, peer review, audit enhancements, i18n
Features implemented:
- F1: Email digest notifications with cron endpoint and per-user frequency
- F2: Jury availability windows and workload preferences in smart assignment
- F3: Round templates with save-from-round and CRUD management
- F4: Side-by-side project comparison view for jury members
- F5: Real-time voting dashboard with Server-Sent Events (SSE)
- F6: Live voting UX: QR codes, audience voting, tie-breaking, score animations
- F7: File versioning, inline preview, bulk download with presigned URLs
- F8: Mentor dashboard: milestones, private notes, activity tracking
- F9: Communication hub with broadcasts, templates, and recipient targeting
- F10: Advanced analytics: cross-round comparison, juror consistency, diversity metrics, PDF export
- F11: Applicant draft saving with magic link resume and cron cleanup
- F12: Webhook integration layer with HMAC signing, retry, and delivery logs
- F13: Peer review discussions with anonymized scores and threaded comments
- F14: Audit log enhancements: before/after diffs, session grouping, anomaly detection, retention
- F15: i18n foundation with next-intl (EN/FR), cookie-based locale, language switcher
Schema: 12 new models, field additions to User, Project, ProjectFile, LiveVotingSession, LiveVote, MentorAssignment, AuditLog, Program
New routers: roundTemplate, message, webhook (registered in _app.ts)
New services: email-digest, webhook-dispatcher
New cron endpoints: /api/cron/digest, /api/cron/draft-cleanup, /api/cron/audit-cleanup
New API routes: /api/live-voting/stream (SSE), /api/files/bulk-download
All features are admin-configurable via SystemSettings or per-model settingsJson fields.
Docker build verified successfully.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-05 23:31:41 +01:00
|
|
|
const audienceWeight = session.audienceVoteWeight || 0
|
|
|
|
|
const juryWeight = 1 - audienceWeight
|
|
|
|
|
|
|
|
|
|
// Get jury votes grouped by project
|
|
|
|
|
const juryScores = await ctx.prisma.liveVote.groupBy({
|
2026-01-30 13:41:32 +01:00
|
|
|
by: ['projectId'],
|
Implement 15 platform features: digest, availability, templates, comparison, live voting SSE, file versioning, mentorship, messaging, analytics, drafts, webhooks, peer review, audit enhancements, i18n
Features implemented:
- F1: Email digest notifications with cron endpoint and per-user frequency
- F2: Jury availability windows and workload preferences in smart assignment
- F3: Round templates with save-from-round and CRUD management
- F4: Side-by-side project comparison view for jury members
- F5: Real-time voting dashboard with Server-Sent Events (SSE)
- F6: Live voting UX: QR codes, audience voting, tie-breaking, score animations
- F7: File versioning, inline preview, bulk download with presigned URLs
- F8: Mentor dashboard: milestones, private notes, activity tracking
- F9: Communication hub with broadcasts, templates, and recipient targeting
- F10: Advanced analytics: cross-round comparison, juror consistency, diversity metrics, PDF export
- F11: Applicant draft saving with magic link resume and cron cleanup
- F12: Webhook integration layer with HMAC signing, retry, and delivery logs
- F13: Peer review discussions with anonymized scores and threaded comments
- F14: Audit log enhancements: before/after diffs, session grouping, anomaly detection, retention
- F15: i18n foundation with next-intl (EN/FR), cookie-based locale, language switcher
Schema: 12 new models, field additions to User, Project, ProjectFile, LiveVotingSession, LiveVote, MentorAssignment, AuditLog, Program
New routers: roundTemplate, message, webhook (registered in _app.ts)
New services: email-digest, webhook-dispatcher
New cron endpoints: /api/cron/digest, /api/cron/draft-cleanup, /api/cron/audit-cleanup
New API routes: /api/live-voting/stream (SSE), /api/files/bulk-download
All features are admin-configurable via SystemSettings or per-model settingsJson fields.
Docker build verified successfully.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-05 23:31:41 +01:00
|
|
|
where: { sessionId: input.sessionId, isAudienceVote: false },
|
|
|
|
|
_avg: { score: true },
|
|
|
|
|
_count: true,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Get audience votes grouped by project
|
|
|
|
|
const audienceScores = await ctx.prisma.liveVote.groupBy({
|
|
|
|
|
by: ['projectId'],
|
|
|
|
|
where: { sessionId: input.sessionId, isAudienceVote: true },
|
2026-01-30 13:41:32 +01:00
|
|
|
_avg: { score: true },
|
|
|
|
|
_count: true,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Get project details
|
Implement 15 platform features: digest, availability, templates, comparison, live voting SSE, file versioning, mentorship, messaging, analytics, drafts, webhooks, peer review, audit enhancements, i18n
Features implemented:
- F1: Email digest notifications with cron endpoint and per-user frequency
- F2: Jury availability windows and workload preferences in smart assignment
- F3: Round templates with save-from-round and CRUD management
- F4: Side-by-side project comparison view for jury members
- F5: Real-time voting dashboard with Server-Sent Events (SSE)
- F6: Live voting UX: QR codes, audience voting, tie-breaking, score animations
- F7: File versioning, inline preview, bulk download with presigned URLs
- F8: Mentor dashboard: milestones, private notes, activity tracking
- F9: Communication hub with broadcasts, templates, and recipient targeting
- F10: Advanced analytics: cross-round comparison, juror consistency, diversity metrics, PDF export
- F11: Applicant draft saving with magic link resume and cron cleanup
- F12: Webhook integration layer with HMAC signing, retry, and delivery logs
- F13: Peer review discussions with anonymized scores and threaded comments
- F14: Audit log enhancements: before/after diffs, session grouping, anomaly detection, retention
- F15: i18n foundation with next-intl (EN/FR), cookie-based locale, language switcher
Schema: 12 new models, field additions to User, Project, ProjectFile, LiveVotingSession, LiveVote, MentorAssignment, AuditLog, Program
New routers: roundTemplate, message, webhook (registered in _app.ts)
New services: email-digest, webhook-dispatcher
New cron endpoints: /api/cron/digest, /api/cron/draft-cleanup, /api/cron/audit-cleanup
New API routes: /api/live-voting/stream (SSE), /api/files/bulk-download
All features are admin-configurable via SystemSettings or per-model settingsJson fields.
Docker build verified successfully.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-05 23:31:41 +01:00
|
|
|
const allProjectIds = [
|
|
|
|
|
...new Set([
|
|
|
|
|
...juryScores.map((s) => s.projectId),
|
|
|
|
|
...audienceScores.map((s) => s.projectId),
|
|
|
|
|
]),
|
|
|
|
|
]
|
2026-01-30 13:41:32 +01:00
|
|
|
const projects = await ctx.prisma.project.findMany({
|
Implement 15 platform features: digest, availability, templates, comparison, live voting SSE, file versioning, mentorship, messaging, analytics, drafts, webhooks, peer review, audit enhancements, i18n
Features implemented:
- F1: Email digest notifications with cron endpoint and per-user frequency
- F2: Jury availability windows and workload preferences in smart assignment
- F3: Round templates with save-from-round and CRUD management
- F4: Side-by-side project comparison view for jury members
- F5: Real-time voting dashboard with Server-Sent Events (SSE)
- F6: Live voting UX: QR codes, audience voting, tie-breaking, score animations
- F7: File versioning, inline preview, bulk download with presigned URLs
- F8: Mentor dashboard: milestones, private notes, activity tracking
- F9: Communication hub with broadcasts, templates, and recipient targeting
- F10: Advanced analytics: cross-round comparison, juror consistency, diversity metrics, PDF export
- F11: Applicant draft saving with magic link resume and cron cleanup
- F12: Webhook integration layer with HMAC signing, retry, and delivery logs
- F13: Peer review discussions with anonymized scores and threaded comments
- F14: Audit log enhancements: before/after diffs, session grouping, anomaly detection, retention
- F15: i18n foundation with next-intl (EN/FR), cookie-based locale, language switcher
Schema: 12 new models, field additions to User, Project, ProjectFile, LiveVotingSession, LiveVote, MentorAssignment, AuditLog, Program
New routers: roundTemplate, message, webhook (registered in _app.ts)
New services: email-digest, webhook-dispatcher
New cron endpoints: /api/cron/digest, /api/cron/draft-cleanup, /api/cron/audit-cleanup
New API routes: /api/live-voting/stream (SSE), /api/files/bulk-download
All features are admin-configurable via SystemSettings or per-model settingsJson fields.
Docker build verified successfully.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-05 23:31:41 +01:00
|
|
|
where: { id: { in: allProjectIds } },
|
2026-01-30 13:41:32 +01:00
|
|
|
select: { id: true, title: true, teamName: true },
|
|
|
|
|
})
|
|
|
|
|
|
Implement 15 platform features: digest, availability, templates, comparison, live voting SSE, file versioning, mentorship, messaging, analytics, drafts, webhooks, peer review, audit enhancements, i18n
Features implemented:
- F1: Email digest notifications with cron endpoint and per-user frequency
- F2: Jury availability windows and workload preferences in smart assignment
- F3: Round templates with save-from-round and CRUD management
- F4: Side-by-side project comparison view for jury members
- F5: Real-time voting dashboard with Server-Sent Events (SSE)
- F6: Live voting UX: QR codes, audience voting, tie-breaking, score animations
- F7: File versioning, inline preview, bulk download with presigned URLs
- F8: Mentor dashboard: milestones, private notes, activity tracking
- F9: Communication hub with broadcasts, templates, and recipient targeting
- F10: Advanced analytics: cross-round comparison, juror consistency, diversity metrics, PDF export
- F11: Applicant draft saving with magic link resume and cron cleanup
- F12: Webhook integration layer with HMAC signing, retry, and delivery logs
- F13: Peer review discussions with anonymized scores and threaded comments
- F14: Audit log enhancements: before/after diffs, session grouping, anomaly detection, retention
- F15: i18n foundation with next-intl (EN/FR), cookie-based locale, language switcher
Schema: 12 new models, field additions to User, Project, ProjectFile, LiveVotingSession, LiveVote, MentorAssignment, AuditLog, Program
New routers: roundTemplate, message, webhook (registered in _app.ts)
New services: email-digest, webhook-dispatcher
New cron endpoints: /api/cron/digest, /api/cron/draft-cleanup, /api/cron/audit-cleanup
New API routes: /api/live-voting/stream (SSE), /api/files/bulk-download
All features are admin-configurable via SystemSettings or per-model settingsJson fields.
Docker build verified successfully.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-05 23:31:41 +01:00
|
|
|
const audienceMap = new Map(audienceScores.map((s) => [s.projectId, s]))
|
|
|
|
|
|
|
|
|
|
// Combine and calculate weighted scores
|
|
|
|
|
const results = juryScores
|
|
|
|
|
.map((jurySc) => {
|
|
|
|
|
const project = projects.find((p) => p.id === jurySc.projectId)
|
|
|
|
|
const audienceSc = audienceMap.get(jurySc.projectId)
|
Fix build errors: add missing Prisma models/fields and resolve TypeScript type errors
Schema: Add 11 new models (RoundTemplate, MentorNote, MentorMilestone,
MentorMilestoneCompletion, EvaluationDiscussion, DiscussionComment,
Message, MessageRecipient, MessageTemplate, Webhook, WebhookDelivery,
DigestLog) and missing fields on existing models (Project.isDraft,
ProjectFile.version, LiveVotingSession.allowAudienceVotes, User.digestFrequency,
AuditLog.sessionId, MentorAssignment.completionStatus, etc).
Add AUDIT_CONFIG/LOCALIZATION/DIGEST/ANALYTICS enum values.
Code: Fix implicit any types, route type casts, enum casts, null safety,
composite key handling, and relation field names across 11 source files.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 14:04:02 +01:00
|
|
|
const juryAvg = jurySc._avg?.score || 0
|
|
|
|
|
const audienceAvg = audienceSc?._avg?.score || 0
|
Implement 15 platform features: digest, availability, templates, comparison, live voting SSE, file versioning, mentorship, messaging, analytics, drafts, webhooks, peer review, audit enhancements, i18n
Features implemented:
- F1: Email digest notifications with cron endpoint and per-user frequency
- F2: Jury availability windows and workload preferences in smart assignment
- F3: Round templates with save-from-round and CRUD management
- F4: Side-by-side project comparison view for jury members
- F5: Real-time voting dashboard with Server-Sent Events (SSE)
- F6: Live voting UX: QR codes, audience voting, tie-breaking, score animations
- F7: File versioning, inline preview, bulk download with presigned URLs
- F8: Mentor dashboard: milestones, private notes, activity tracking
- F9: Communication hub with broadcasts, templates, and recipient targeting
- F10: Advanced analytics: cross-round comparison, juror consistency, diversity metrics, PDF export
- F11: Applicant draft saving with magic link resume and cron cleanup
- F12: Webhook integration layer with HMAC signing, retry, and delivery logs
- F13: Peer review discussions with anonymized scores and threaded comments
- F14: Audit log enhancements: before/after diffs, session grouping, anomaly detection, retention
- F15: i18n foundation with next-intl (EN/FR), cookie-based locale, language switcher
Schema: 12 new models, field additions to User, Project, ProjectFile, LiveVotingSession, LiveVote, MentorAssignment, AuditLog, Program
New routers: roundTemplate, message, webhook (registered in _app.ts)
New services: email-digest, webhook-dispatcher
New cron endpoints: /api/cron/digest, /api/cron/draft-cleanup, /api/cron/audit-cleanup
New API routes: /api/live-voting/stream (SSE), /api/files/bulk-download
All features are admin-configurable via SystemSettings or per-model settingsJson fields.
Docker build verified successfully.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-05 23:31:41 +01:00
|
|
|
const weightedTotal = audienceWeight > 0 && audienceSc
|
|
|
|
|
? juryAvg * juryWeight + audienceAvg * audienceWeight
|
|
|
|
|
: juryAvg
|
|
|
|
|
|
2026-01-30 13:41:32 +01:00
|
|
|
return {
|
|
|
|
|
project,
|
Implement 15 platform features: digest, availability, templates, comparison, live voting SSE, file versioning, mentorship, messaging, analytics, drafts, webhooks, peer review, audit enhancements, i18n
Features implemented:
- F1: Email digest notifications with cron endpoint and per-user frequency
- F2: Jury availability windows and workload preferences in smart assignment
- F3: Round templates with save-from-round and CRUD management
- F4: Side-by-side project comparison view for jury members
- F5: Real-time voting dashboard with Server-Sent Events (SSE)
- F6: Live voting UX: QR codes, audience voting, tie-breaking, score animations
- F7: File versioning, inline preview, bulk download with presigned URLs
- F8: Mentor dashboard: milestones, private notes, activity tracking
- F9: Communication hub with broadcasts, templates, and recipient targeting
- F10: Advanced analytics: cross-round comparison, juror consistency, diversity metrics, PDF export
- F11: Applicant draft saving with magic link resume and cron cleanup
- F12: Webhook integration layer with HMAC signing, retry, and delivery logs
- F13: Peer review discussions with anonymized scores and threaded comments
- F14: Audit log enhancements: before/after diffs, session grouping, anomaly detection, retention
- F15: i18n foundation with next-intl (EN/FR), cookie-based locale, language switcher
Schema: 12 new models, field additions to User, Project, ProjectFile, LiveVotingSession, LiveVote, MentorAssignment, AuditLog, Program
New routers: roundTemplate, message, webhook (registered in _app.ts)
New services: email-digest, webhook-dispatcher
New cron endpoints: /api/cron/digest, /api/cron/draft-cleanup, /api/cron/audit-cleanup
New API routes: /api/live-voting/stream (SSE), /api/files/bulk-download
All features are admin-configurable via SystemSettings or per-model settingsJson fields.
Docker build verified successfully.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-05 23:31:41 +01:00
|
|
|
juryAverage: juryAvg,
|
|
|
|
|
juryVoteCount: jurySc._count,
|
|
|
|
|
audienceAverage: audienceAvg,
|
|
|
|
|
audienceVoteCount: audienceSc?._count || 0,
|
|
|
|
|
weightedTotal,
|
2026-01-30 13:41:32 +01:00
|
|
|
}
|
|
|
|
|
})
|
Implement 15 platform features: digest, availability, templates, comparison, live voting SSE, file versioning, mentorship, messaging, analytics, drafts, webhooks, peer review, audit enhancements, i18n
Features implemented:
- F1: Email digest notifications with cron endpoint and per-user frequency
- F2: Jury availability windows and workload preferences in smart assignment
- F3: Round templates with save-from-round and CRUD management
- F4: Side-by-side project comparison view for jury members
- F5: Real-time voting dashboard with Server-Sent Events (SSE)
- F6: Live voting UX: QR codes, audience voting, tie-breaking, score animations
- F7: File versioning, inline preview, bulk download with presigned URLs
- F8: Mentor dashboard: milestones, private notes, activity tracking
- F9: Communication hub with broadcasts, templates, and recipient targeting
- F10: Advanced analytics: cross-round comparison, juror consistency, diversity metrics, PDF export
- F11: Applicant draft saving with magic link resume and cron cleanup
- F12: Webhook integration layer with HMAC signing, retry, and delivery logs
- F13: Peer review discussions with anonymized scores and threaded comments
- F14: Audit log enhancements: before/after diffs, session grouping, anomaly detection, retention
- F15: i18n foundation with next-intl (EN/FR), cookie-based locale, language switcher
Schema: 12 new models, field additions to User, Project, ProjectFile, LiveVotingSession, LiveVote, MentorAssignment, AuditLog, Program
New routers: roundTemplate, message, webhook (registered in _app.ts)
New services: email-digest, webhook-dispatcher
New cron endpoints: /api/cron/digest, /api/cron/draft-cleanup, /api/cron/audit-cleanup
New API routes: /api/live-voting/stream (SSE), /api/files/bulk-download
All features are admin-configurable via SystemSettings or per-model settingsJson fields.
Docker build verified successfully.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-05 23:31:41 +01:00
|
|
|
.sort((a, b) => b.weightedTotal - a.weightedTotal)
|
|
|
|
|
|
|
|
|
|
// Detect ties
|
|
|
|
|
const ties: string[][] = []
|
|
|
|
|
for (let i = 0; i < results.length - 1; i++) {
|
|
|
|
|
if (Math.abs(results[i].weightedTotal - results[i + 1].weightedTotal) < 0.001) {
|
|
|
|
|
const tieGroup = [results[i].project?.id, results[i + 1].project?.id].filter(Boolean) as string[]
|
|
|
|
|
ties.push(tieGroup)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-30 13:41:32 +01:00
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
session,
|
|
|
|
|
results,
|
Implement 15 platform features: digest, availability, templates, comparison, live voting SSE, file versioning, mentorship, messaging, analytics, drafts, webhooks, peer review, audit enhancements, i18n
Features implemented:
- F1: Email digest notifications with cron endpoint and per-user frequency
- F2: Jury availability windows and workload preferences in smart assignment
- F3: Round templates with save-from-round and CRUD management
- F4: Side-by-side project comparison view for jury members
- F5: Real-time voting dashboard with Server-Sent Events (SSE)
- F6: Live voting UX: QR codes, audience voting, tie-breaking, score animations
- F7: File versioning, inline preview, bulk download with presigned URLs
- F8: Mentor dashboard: milestones, private notes, activity tracking
- F9: Communication hub with broadcasts, templates, and recipient targeting
- F10: Advanced analytics: cross-round comparison, juror consistency, diversity metrics, PDF export
- F11: Applicant draft saving with magic link resume and cron cleanup
- F12: Webhook integration layer with HMAC signing, retry, and delivery logs
- F13: Peer review discussions with anonymized scores and threaded comments
- F14: Audit log enhancements: before/after diffs, session grouping, anomaly detection, retention
- F15: i18n foundation with next-intl (EN/FR), cookie-based locale, language switcher
Schema: 12 new models, field additions to User, Project, ProjectFile, LiveVotingSession, LiveVote, MentorAssignment, AuditLog, Program
New routers: roundTemplate, message, webhook (registered in _app.ts)
New services: email-digest, webhook-dispatcher
New cron endpoints: /api/cron/digest, /api/cron/draft-cleanup, /api/cron/audit-cleanup
New API routes: /api/live-voting/stream (SSE), /api/files/bulk-download
All features are admin-configurable via SystemSettings or per-model settingsJson fields.
Docker build verified successfully.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-05 23:31:41 +01:00
|
|
|
ties,
|
|
|
|
|
tieBreakerMethod: session.tieBreakerMethod,
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Update presentation settings for a live voting session
|
|
|
|
|
*/
|
|
|
|
|
updatePresentationSettings: adminProcedure
|
|
|
|
|
.input(
|
|
|
|
|
z.object({
|
|
|
|
|
sessionId: z.string(),
|
|
|
|
|
presentationSettingsJson: z.object({
|
|
|
|
|
theme: z.string().optional(),
|
|
|
|
|
autoAdvance: z.boolean().optional(),
|
|
|
|
|
autoAdvanceDelay: z.number().int().min(5).max(120).optional(),
|
|
|
|
|
scoreDisplayFormat: z.enum(['bar', 'number', 'radial']).optional(),
|
|
|
|
|
showVoteCount: z.boolean().optional(),
|
|
|
|
|
brandingOverlay: z.string().optional(),
|
|
|
|
|
}),
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
.mutation(async ({ ctx, input }) => {
|
|
|
|
|
const session = await ctx.prisma.liveVotingSession.update({
|
|
|
|
|
where: { id: input.sessionId },
|
|
|
|
|
data: {
|
|
|
|
|
presentationSettingsJson: input.presentationSettingsJson,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return session
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Update session config (audience voting, tie-breaker)
|
|
|
|
|
*/
|
|
|
|
|
updateSessionConfig: adminProcedure
|
|
|
|
|
.input(
|
|
|
|
|
z.object({
|
|
|
|
|
sessionId: z.string(),
|
|
|
|
|
allowAudienceVotes: z.boolean().optional(),
|
|
|
|
|
audienceVoteWeight: z.number().min(0).max(1).optional(),
|
|
|
|
|
tieBreakerMethod: z.enum(['admin_decides', 'highest_individual', 'revote']).optional(),
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
.mutation(async ({ ctx, input }) => {
|
|
|
|
|
const { sessionId, ...data } = input
|
|
|
|
|
|
|
|
|
|
const session = await ctx.prisma.liveVotingSession.update({
|
|
|
|
|
where: { id: sessionId },
|
|
|
|
|
data,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Audit log
|
|
|
|
|
try {
|
|
|
|
|
await logAudit({
|
|
|
|
|
prisma: ctx.prisma,
|
|
|
|
|
userId: ctx.user.id,
|
|
|
|
|
action: 'UPDATE_SESSION_CONFIG',
|
|
|
|
|
entityType: 'LiveVotingSession',
|
|
|
|
|
entityId: sessionId,
|
|
|
|
|
detailsJson: data,
|
|
|
|
|
ipAddress: ctx.ip,
|
|
|
|
|
userAgent: ctx.userAgent,
|
|
|
|
|
})
|
|
|
|
|
} catch {
|
|
|
|
|
// Audit log errors should never break the operation
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return session
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Cast an audience vote
|
|
|
|
|
*/
|
|
|
|
|
castAudienceVote: protectedProcedure
|
|
|
|
|
.input(
|
|
|
|
|
z.object({
|
|
|
|
|
sessionId: z.string(),
|
|
|
|
|
projectId: z.string(),
|
|
|
|
|
score: z.number().int().min(1).max(10),
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
.mutation(async ({ ctx, input }) => {
|
|
|
|
|
// Verify session is in progress and allows audience votes
|
|
|
|
|
const session = await ctx.prisma.liveVotingSession.findUniqueOrThrow({
|
|
|
|
|
where: { id: input.sessionId },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (session.status !== 'IN_PROGRESS') {
|
|
|
|
|
throw new TRPCError({
|
|
|
|
|
code: 'BAD_REQUEST',
|
|
|
|
|
message: 'Voting is not currently active',
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!session.allowAudienceVotes) {
|
|
|
|
|
throw new TRPCError({
|
|
|
|
|
code: 'FORBIDDEN',
|
|
|
|
|
message: 'Audience voting is not enabled for this session',
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (session.currentProjectId !== input.projectId) {
|
|
|
|
|
throw new TRPCError({
|
|
|
|
|
code: 'BAD_REQUEST',
|
|
|
|
|
message: 'Cannot vote for this project right now',
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (session.votingEndsAt && new Date() > session.votingEndsAt) {
|
|
|
|
|
throw new TRPCError({
|
|
|
|
|
code: 'BAD_REQUEST',
|
|
|
|
|
message: 'Voting window has closed',
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Upsert audience vote
|
|
|
|
|
const vote = await ctx.prisma.liveVote.upsert({
|
|
|
|
|
where: {
|
|
|
|
|
sessionId_projectId_userId: {
|
|
|
|
|
sessionId: input.sessionId,
|
|
|
|
|
projectId: input.projectId,
|
|
|
|
|
userId: ctx.user.id,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
create: {
|
|
|
|
|
sessionId: input.sessionId,
|
|
|
|
|
projectId: input.projectId,
|
|
|
|
|
userId: ctx.user.id,
|
|
|
|
|
score: input.score,
|
|
|
|
|
isAudienceVote: true,
|
|
|
|
|
},
|
|
|
|
|
update: {
|
|
|
|
|
score: input.score,
|
|
|
|
|
votedAt: new Date(),
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return vote
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get public results for a live voting session (no auth required)
|
|
|
|
|
*/
|
|
|
|
|
getPublicResults: publicProcedure
|
|
|
|
|
.input(z.object({ sessionId: z.string() }))
|
|
|
|
|
.query(async ({ ctx, input }) => {
|
|
|
|
|
const session = await ctx.prisma.liveVotingSession.findUniqueOrThrow({
|
|
|
|
|
where: { id: input.sessionId },
|
|
|
|
|
select: {
|
|
|
|
|
id: true,
|
|
|
|
|
status: true,
|
|
|
|
|
currentProjectId: true,
|
|
|
|
|
votingEndsAt: true,
|
|
|
|
|
presentationSettingsJson: true,
|
|
|
|
|
allowAudienceVotes: true,
|
|
|
|
|
audienceVoteWeight: true,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Only return data if session is in progress or completed
|
|
|
|
|
if (session.status !== 'IN_PROGRESS' && session.status !== 'COMPLETED') {
|
|
|
|
|
return {
|
|
|
|
|
session: {
|
|
|
|
|
id: session.id,
|
|
|
|
|
status: session.status,
|
|
|
|
|
presentationSettings: session.presentationSettingsJson,
|
|
|
|
|
},
|
|
|
|
|
projects: [],
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get all votes grouped by project (anonymized - no user data)
|
|
|
|
|
const scores = await ctx.prisma.liveVote.groupBy({
|
|
|
|
|
by: ['projectId'],
|
|
|
|
|
where: { sessionId: input.sessionId },
|
|
|
|
|
_avg: { score: true },
|
|
|
|
|
_count: true,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const projectIds = scores.map((s) => s.projectId)
|
|
|
|
|
const projects = await ctx.prisma.project.findMany({
|
|
|
|
|
where: { id: { in: projectIds } },
|
|
|
|
|
select: { id: true, title: true, teamName: true },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const projectsWithScores = scores.map((score) => {
|
|
|
|
|
const project = projects.find((p) => p.id === score.projectId)
|
|
|
|
|
return {
|
|
|
|
|
id: project?.id,
|
|
|
|
|
title: project?.title,
|
|
|
|
|
teamName: project?.teamName,
|
|
|
|
|
averageScore: score._avg.score || 0,
|
|
|
|
|
voteCount: score._count,
|
|
|
|
|
}
|
|
|
|
|
}).sort((a, b) => b.averageScore - a.averageScore)
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
session: {
|
|
|
|
|
id: session.id,
|
|
|
|
|
status: session.status,
|
|
|
|
|
currentProjectId: session.currentProjectId,
|
|
|
|
|
votingEndsAt: session.votingEndsAt,
|
|
|
|
|
presentationSettings: session.presentationSettingsJson,
|
|
|
|
|
allowAudienceVotes: session.allowAudienceVotes,
|
|
|
|
|
},
|
|
|
|
|
projects: projectsWithScores,
|
2026-01-30 13:41:32 +01:00
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
})
|