This commit is contained in:
1 parent
8efd032cc6
commit
df38dccbf1
735 files changed
+128321
-120870
No files matched your search
@@ -7,141 +7,152 @@ import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export default async function ModTeamPage() {
|
||||
const { session, permissions } = await getAdminContext();
|
||||
if (!canAccess(permissions, PERMS.MODERATION_VIEW, session.user.rank)) redirect("/admin");
|
||||
const { session, permissions } = await getAdminContext();
|
||||
if (!canAccess(permissions, PERMS.MODERATION_VIEW, session.user.rank))
|
||||
redirect("/admin");
|
||||
|
||||
// Get all staff (rank >= 4)
|
||||
const staff = await prisma.user.findMany({
|
||||
where: { rank: { gte: 4 } },
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
look: true,
|
||||
rank: true,
|
||||
online: true,
|
||||
lastLogin: true,
|
||||
lastOnline: true,
|
||||
},
|
||||
orderBy: [{ online: "desc" }, { rank: "desc" }],
|
||||
});
|
||||
// Get all staff (rank >= 4)
|
||||
const staff = await prisma.user.findMany({
|
||||
where: { rank: { gte: 4 } },
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
look: true,
|
||||
rank: true,
|
||||
online: true,
|
||||
lastLogin: true,
|
||||
lastOnline: true,
|
||||
},
|
||||
orderBy: [{ online: "desc" }, { rank: "desc" }],
|
||||
});
|
||||
|
||||
// Get CFH ticket counts per mod and web ticket counts
|
||||
const [cfhCounts, webTicketCounts, modActionCounts] = await Promise.all([
|
||||
prisma.supportTickets.groupBy({
|
||||
by: ["modId"],
|
||||
where: { modId: { in: staff.map((s) => s.id) } },
|
||||
_count: { _all: true },
|
||||
}),
|
||||
prisma.websiteTicket.groupBy({
|
||||
by: ["assigneeId"],
|
||||
where: { assigneeId: { in: staff.map((s) => s.id) } },
|
||||
_count: { _all: true },
|
||||
}),
|
||||
prisma.adminAuditLog.groupBy({
|
||||
by: ["userId"],
|
||||
where: {
|
||||
userId: { in: staff.map((s) => s.id) },
|
||||
action: { startsWith: "mod_" },
|
||||
},
|
||||
_count: { _all: true },
|
||||
}),
|
||||
]);
|
||||
// Get CFH ticket counts per mod and web ticket counts
|
||||
const [cfhCounts, webTicketCounts, modActionCounts] = await Promise.all([
|
||||
prisma.supportTickets.groupBy({
|
||||
by: ["modId"],
|
||||
where: { modId: { in: staff.map((s) => s.id) } },
|
||||
_count: { _all: true },
|
||||
}),
|
||||
prisma.websiteTicket.groupBy({
|
||||
by: ["assigneeId"],
|
||||
where: { assigneeId: { in: staff.map((s) => s.id) } },
|
||||
_count: { _all: true },
|
||||
}),
|
||||
prisma.adminAuditLog.groupBy({
|
||||
by: ["userId"],
|
||||
where: {
|
||||
userId: { in: staff.map((s) => s.id) },
|
||||
action: { startsWith: "mod_" },
|
||||
},
|
||||
_count: { _all: true },
|
||||
}),
|
||||
]);
|
||||
|
||||
const cfhMap = new Map(cfhCounts.map((c) => [c.modId, c._count._all]));
|
||||
const webMap = new Map(webTicketCounts.map((c) => [c.assigneeId, c._count._all]));
|
||||
const actionMap = new Map(modActionCounts.map((c) => [c.userId, c._count._all]));
|
||||
const cfhMap = new Map(cfhCounts.map((c) => [c.modId, c._count._all]));
|
||||
const webMap = new Map(
|
||||
webTicketCounts.map((c) => [c.assigneeId, c._count._all]),
|
||||
);
|
||||
const actionMap = new Map(
|
||||
modActionCounts.map((c) => [c.userId, c._count._all]),
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Mod Team</h1>
|
||||
<p className="text-muted-foreground">Staff performance and status</p>
|
||||
</div>
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Mod Team</h1>
|
||||
<p className="text-muted-foreground">Staff performance and status</p>
|
||||
</div>
|
||||
|
||||
{/* Summary */}
|
||||
<div className="grid gap-4 grid-cols-3">
|
||||
<Card>
|
||||
<CardContent className="pt-4 text-center">
|
||||
<p className="text-2xl font-bold text-[var(--admin-success)]">
|
||||
{staff.filter((s) => s.online === "1").length}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">Online Now</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="pt-4 text-center">
|
||||
<p className="text-2xl font-bold">{staff.length}</p>
|
||||
<p className="text-xs text-muted-foreground">Total Staff</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="pt-4 text-center">
|
||||
<p className="text-2xl font-bold text-primary">{staff.filter((s) => s.rank >= 7).length}</p>
|
||||
<p className="text-xs text-muted-foreground">Super Admins</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
{/* Summary */}
|
||||
<div className="grid gap-4 grid-cols-3">
|
||||
<Card>
|
||||
<CardContent className="pt-4 text-center">
|
||||
<p className="text-2xl font-bold text-[var(--admin-success)]">
|
||||
{staff.filter((s) => s.online === "1").length}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">Online Now</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="pt-4 text-center">
|
||||
<p className="text-2xl font-bold">{staff.length}</p>
|
||||
<p className="text-xs text-muted-foreground">Total Staff</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="pt-4 text-center">
|
||||
<p className="text-2xl font-bold text-primary">
|
||||
{staff.filter((s) => s.rank >= 7).length}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">Super Admins</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Staff list */}
|
||||
<div className="rounded-lg border">
|
||||
<div className="grid grid-cols-[auto_1fr_auto_auto_auto_auto_auto] gap-4 p-3 bg-muted/50 text-xs font-medium text-muted-foreground border-b">
|
||||
<span></span>
|
||||
<span>Name</span>
|
||||
<span>Rank</span>
|
||||
<span>CFH</span>
|
||||
<span>Tickets</span>
|
||||
<span>Actions</span>
|
||||
<span>Last Seen</span>
|
||||
</div>
|
||||
{/* Staff list */}
|
||||
<div className="rounded-lg border">
|
||||
<div className="grid grid-cols-[auto_1fr_auto_auto_auto_auto_auto] gap-4 p-3 bg-muted/50 text-xs font-medium text-muted-foreground border-b">
|
||||
<span></span>
|
||||
<span>Name</span>
|
||||
<span>Rank</span>
|
||||
<span>CFH</span>
|
||||
<span>Tickets</span>
|
||||
<span>Actions</span>
|
||||
<span>Last Seen</span>
|
||||
</div>
|
||||
|
||||
{staff.map((member) => (
|
||||
<Link
|
||||
key={member.id}
|
||||
href={`/admin/users/show/${member.id}`}
|
||||
className="grid grid-cols-[auto_1fr_auto_auto_auto_auto_auto] gap-4 p-3 items-center hover:bg-accent/50 transition-all duration-150 border-b last:border-0"
|
||||
>
|
||||
<div className="relative">
|
||||
<AvatarImage
|
||||
figure={member.look}
|
||||
options={{ headOnly: true, size: "l" }}
|
||||
alt={member.username}
|
||||
width={32}
|
||||
height={32}
|
||||
/>
|
||||
<span
|
||||
className={`absolute -bottom-0.5 -right-0.5 w-2.5 h-2.5 rounded-full border-2 border-background ${
|
||||
member.online === "1" ? "bg-[var(--admin-success)]" : "bg-[var(--admin-text-muted)]"
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium">{member.username}</p>
|
||||
</div>
|
||||
<Badge variant="outline" className="text-[10px]">
|
||||
Rank {member.rank}
|
||||
</Badge>
|
||||
<div className="text-center">
|
||||
<p className="text-sm font-bold">{cfhMap.get(member.id) ?? 0}</p>
|
||||
<p className="text-[9px] text-muted-foreground">CFH</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-sm font-bold">{webMap.get(member.id) ?? 0}</p>
|
||||
<p className="text-[9px] text-muted-foreground">Web</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-sm font-bold">{actionMap.get(member.id) ?? 0}</p>
|
||||
<p className="text-[9px] text-muted-foreground">Mod</p>
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground whitespace-nowrap">
|
||||
{member.online === "1"
|
||||
? "Online"
|
||||
: member.lastOnline
|
||||
? new Date(member.lastOnline * 1000).toLocaleDateString()
|
||||
: "—"}
|
||||
</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
{staff.map((member) => (
|
||||
<Link
|
||||
key={member.id}
|
||||
href={`/admin/users/show/${member.id}`}
|
||||
className="grid grid-cols-[auto_1fr_auto_auto_auto_auto_auto] gap-4 p-3 items-center hover:bg-accent/50 transition-all duration-150 border-b last:border-0"
|
||||
>
|
||||
<div className="relative">
|
||||
<AvatarImage
|
||||
figure={member.look}
|
||||
options={{ headOnly: true, size: "l" }}
|
||||
alt={member.username}
|
||||
width={32}
|
||||
height={32}
|
||||
/>
|
||||
<span
|
||||
className={`absolute -bottom-0.5 -right-0.5 w-2.5 h-2.5 rounded-full border-2 border-background ${
|
||||
member.online === "1"
|
||||
? "bg-[var(--admin-success)]"
|
||||
: "bg-[var(--admin-text-muted)]"
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium">{member.username}</p>
|
||||
</div>
|
||||
<Badge variant="outline" className="text-[10px]">
|
||||
Rank {member.rank}
|
||||
</Badge>
|
||||
<div className="text-center">
|
||||
<p className="text-sm font-bold">{cfhMap.get(member.id) ?? 0}</p>
|
||||
<p className="text-[9px] text-muted-foreground">CFH</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-sm font-bold">{webMap.get(member.id) ?? 0}</p>
|
||||
<p className="text-[9px] text-muted-foreground">Web</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-sm font-bold">
|
||||
{actionMap.get(member.id) ?? 0}
|
||||
</p>
|
||||
<p className="text-[9px] text-muted-foreground">Mod</p>
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground whitespace-nowrap">
|
||||
{member.online === "1"
|
||||
? "Online"
|
||||
: member.lastOnline
|
||||
? new Date(member.lastOnline * 1000).toLocaleDateString()
|
||||
: "—"}
|
||||
</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user