155 lines
4.5 KiB
TypeScript
155 lines
4.5 KiB
TypeScript
import config from "@payload-config";
|
|
import { isPayloadUser } from "@/utils/access-control/isPayloadUser";
|
|
import { getPayload } from "payload";
|
|
import { headers as nextHeaders } from "next/headers";
|
|
import type { Media, Rank, User } from "@/payload-types";
|
|
import { UsersIcon } from "lucide-react";
|
|
import { RosterView } from "@/components/frontend/roster/RosterView";
|
|
|
|
export const metadata = {
|
|
title: "Unit Roster — Polaris Task Force",
|
|
};
|
|
|
|
export type RosterMember = {
|
|
profileId: number;
|
|
user: Pick<User, "id" | "displayName" | "username">;
|
|
enlistmentDate: string | null;
|
|
xp: number;
|
|
rank: Pick<Rank, "id" | "name" | "abbreviation" | "_order"> & {
|
|
insigniaUrl?: string | null;
|
|
};
|
|
assignmentName?: string;
|
|
assignmentType?: string;
|
|
assignmentCallsign?: string;
|
|
isLeader: boolean;
|
|
};
|
|
|
|
export type RosterAssignment = {
|
|
id: number;
|
|
name: string;
|
|
type: string;
|
|
callsign?: string;
|
|
leaderId?: number;
|
|
memberIds: number[];
|
|
subAssignmentIds: number[];
|
|
};
|
|
|
|
export default async function RosterPage() {
|
|
const headers = await nextHeaders();
|
|
const payload = await getPayload({ config });
|
|
const { user: authUser } = await payload.auth({ headers, canSetHeaders: false });
|
|
const currentUser = isPayloadUser(authUser) ? authUser : null;
|
|
|
|
const [profileRes, assignmentRes] = await Promise.all([
|
|
payload.find({
|
|
collection: "profiles",
|
|
limit: 500,
|
|
depth: 2,
|
|
select: {
|
|
user: true,
|
|
rank: true,
|
|
dossier: true,
|
|
progression: true,
|
|
},
|
|
}),
|
|
payload.find({
|
|
collection: "assignments",
|
|
limit: 200,
|
|
depth: 1,
|
|
select: {
|
|
name: true,
|
|
type: true,
|
|
callsign: true,
|
|
leader: true,
|
|
members: true,
|
|
subAssignments: true,
|
|
},
|
|
}),
|
|
]);
|
|
|
|
const profiles = profileRes.docs;
|
|
|
|
const members: RosterMember[] = profiles.map((profile) => {
|
|
const user = profile.user as User;
|
|
const rank = profile.rank as Rank;
|
|
const insigniaUrl =
|
|
rank.insignia && typeof rank.insignia === "object" ? (rank.insignia as Media).url : null;
|
|
|
|
let assignmentName: string | undefined;
|
|
let assignmentType: string | undefined;
|
|
let assignmentCallsign: string | undefined;
|
|
let isLeader = false;
|
|
|
|
for (const assignment of assignmentRes.docs) {
|
|
const memberIds = (assignment.members ?? [])
|
|
.map((m) => (typeof m === "object" ? m.id : m))
|
|
.filter(Boolean);
|
|
const leaderId =
|
|
assignment.leader && typeof assignment.leader === "object"
|
|
? assignment.leader.id
|
|
: assignment.leader;
|
|
|
|
if (memberIds.includes(user.id) || leaderId === user.id) {
|
|
assignmentName = assignment.name;
|
|
assignmentType = assignment.type;
|
|
assignmentCallsign = assignment.callsign ?? undefined;
|
|
if (leaderId === user.id) isLeader = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return {
|
|
profileId: profile.id,
|
|
user: { id: user.id, displayName: user.displayName, username: user.username },
|
|
enlistmentDate: profile.dossier?.enlistmentDate ?? null,
|
|
xp: profile.progression?.xp ?? 0,
|
|
rank: {
|
|
id: rank.id,
|
|
name: rank.name,
|
|
abbreviation: rank.abbreviation,
|
|
_order: rank._order,
|
|
insigniaUrl,
|
|
},
|
|
assignmentName,
|
|
assignmentType,
|
|
assignmentCallsign,
|
|
isLeader,
|
|
};
|
|
});
|
|
|
|
const assignments: RosterAssignment[] = assignmentRes.docs.map((a) => ({
|
|
id: a.id,
|
|
name: a.name,
|
|
type: a.type,
|
|
callsign: a.callsign ?? undefined,
|
|
leaderId:
|
|
a.leader && typeof a.leader === "object" ? a.leader.id : (a.leader as number | undefined),
|
|
memberIds: (a.members ?? [])
|
|
.map((m) => (typeof m === "object" ? m.id : m))
|
|
.filter(Boolean) as number[],
|
|
subAssignmentIds: (a.subAssignments ?? [])
|
|
.map((s) => (typeof s === "object" ? s.id : s))
|
|
.filter(Boolean) as number[],
|
|
}));
|
|
|
|
return (
|
|
<div className="p-5 flex flex-col gap-6">
|
|
<div className="flex flex-col gap-1">
|
|
<div className="flex items-center gap-2">
|
|
<UsersIcon className="size-5 text-muted-foreground" />
|
|
<h1 className="text-lg font-semibold">Unit Roster</h1>
|
|
</div>
|
|
<p className="text-sm text-muted-foreground">
|
|
{members.length} operator{members.length !== 1 ? "s" : ""} across {assignments.length}{" "}
|
|
assignment{assignments.length !== 1 ? "s" : ""}
|
|
</p>
|
|
</div>
|
|
|
|
<RosterView
|
|
members={members}
|
|
assignments={assignments}
|
|
leadOrActual={currentUser?.preferences?.display?.leadOrActual ?? "lead"}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|