diff --git a/src/app/(admin)/admin/audit/page.tsx b/src/app/(admin)/admin/audit/page.tsx index df1cc8c..a08009e 100644 --- a/src/app/(admin)/admin/audit/page.tsx +++ b/src/app/(admin)/admin/audit/page.tsx @@ -83,6 +83,11 @@ const ACTION_TYPES = [ 'ROLE_CHANGED', 'PASSWORD_SET', 'PASSWORD_CHANGED', + 'JUROR_DROPOUT_RESHUFFLE', + 'COI_REASSIGNMENT', + 'APPLY_AI_SUGGESTIONS', + 'APPLY_SUGGESTIONS', + 'NOTIFY_JURORS_OF_ASSIGNMENTS', ] // Entity type options @@ -118,6 +123,11 @@ const actionColors: Record Details

-
-                                    {JSON.stringify(log.detailsJson, null, 2)}
-                                  
+ {log.action === 'JUROR_DROPOUT_RESHUFFLE' ? ( + } /> + ) : log.action === 'COI_REASSIGNMENT' ? ( + } /> + ) : ( +
+                                      {JSON.stringify(log.detailsJson, null, 2)}
+                                    
+ )} )} {!!(log as Record).previousDataJson && ( @@ -622,9 +638,15 @@ export default function AuditLogPage() {

Details

-
-                              {JSON.stringify(log.detailsJson, null, 2)}
-                            
+ {log.action === 'JUROR_DROPOUT_RESHUFFLE' ? ( + } /> + ) : log.action === 'COI_REASSIGNMENT' ? ( + } /> + ) : ( +
+                                {JSON.stringify(log.detailsJson, null, 2)}
+                              
+ )} )} @@ -693,6 +715,129 @@ export default function AuditLogPage() { ) } +function ReshuffleDetailView({ details }: { details: Record }) { + const reassignedTo = (details.reassignedTo ?? {}) as Record + const jurorIds = Object.keys(reassignedTo) + const moves = (details.moves ?? []) as { projectId: string; projectTitle: string; newJurorId: string; newJurorName: string }[] + + // Resolve juror IDs to names + const { data: nameMap } = trpc.user.resolveNames.useQuery( + { ids: [...jurorIds, details.droppedJurorId as string].filter(Boolean) }, + { enabled: jurorIds.length > 0 }, + ) + + const droppedName = (details.droppedJurorName as string) || (nameMap && details.droppedJurorId ? nameMap[details.droppedJurorId as string] : null) || (details.droppedJurorId as string) + + return ( +
+ {/* Summary */} +
+
+ Juror Dropout + {droppedName} +
+

+ {String(details.movedCount)} project(s) reassigned, {String(details.failedCount)} failed + {details.removedFromGroup ? ' — removed from jury group' : ''} +

+
+ + {/* Per-project moves (new format) */} + {moves.length > 0 && ( +
+

Project → New Juror

+ + + + + + + + + {moves.map((move, i) => ( + + + + + ))} + +
ProjectReassigned To
{move.projectTitle}{move.newJurorName}
+
+ )} + + {/* Fallback: count-based view (old format, no per-project detail) */} + {moves.length === 0 && jurorIds.length > 0 && ( +
+

Reassignment Summary (project detail not available)

+ + + + + + + + + {jurorIds.map((id) => ( + + + + + ))} + +
JurorProjects Received
{nameMap?.[id] || id}{reassignedTo[id]}
+
+ )} + + {/* Failed projects */} + {Array.isArray(details.failedProjects) && (details.failedProjects as string[]).length > 0 && ( +
+

Could not reassign:

+
    + {(details.failedProjects as string[]).map((p, i) => ( +
  • {p}
  • + ))} +
+
+ )} +
+ ) +} + +function COIReassignmentDetailView({ details }: { details: Record }) { + const ids = [details.oldJurorId, details.newJurorId].filter(Boolean) as string[] + const { data: nameMap } = trpc.user.resolveNames.useQuery( + { ids }, + { enabled: ids.length > 0 }, + ) + + const oldName = nameMap?.[details.oldJurorId as string] || (details.oldJurorId as string) + const newName = nameMap?.[details.newJurorId as string] || (details.newJurorId as string) + + return ( +
+
+
+ COI Reassignment +
+
+
+

From

+

{oldName}

+
+
+

To

+

{newName}

+
+
+
+ Project: {(details.projectId as string)?.slice(0, 12)}... + {' | '}Round: {(details.roundId as string)?.slice(0, 12)}... +
+
+
+ ) +} + function DiffViewer({ before, after }: { before: unknown; after: unknown }) { const beforeObj = typeof before === 'object' && before !== null ? before as Record : {} const afterObj = typeof after === 'object' && after !== null ? after as Record : {} diff --git a/src/server/routers/user.ts b/src/server/routers/user.ts index 51f3189..6ce205d 100644 --- a/src/server/routers/user.ts +++ b/src/server/routers/user.ts @@ -360,6 +360,25 @@ export const userRouter = router({ return user }), + /** + * Resolve a batch of user IDs to names (admin only). + * Returns a map of id → name for displaying in audit logs, etc. + */ + resolveNames: adminProcedure + .input(z.object({ ids: z.array(z.string()).max(50) })) + .query(async ({ ctx, input }) => { + if (input.ids.length === 0) return {} + const users = await ctx.prisma.user.findMany({ + where: { id: { in: input.ids } }, + select: { id: true, name: true, email: true }, + }) + const map: Record = {} + for (const u of users) { + map[u.id] = u.name || u.email + } + return map + }), + /** * Create/invite a new user (admin only) */