1
0
Fork 0
polaris-task-force/src/app/(frontend)/roster/page.tsx

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>
);
}