248 lines
8.2 KiB
TypeScript
248 lines
8.2 KiB
TypeScript
import config from "@payload-config";
|
|
import { getPayload } from "payload";
|
|
import { headers as nextHeaders } from "next/headers";
|
|
import type { Assignment, Campaign, GameStructure, Map, Media, Rank, User } from "@/payload-types";
|
|
import { resolveLevel } from "@/utils/xp/resolveLevel";
|
|
import { hasIntelligenceQualification } from "@/utils/access-control/hasIntelligenceQualification";
|
|
import { ProfileSummary } from "@/components/frontend/dashboard/ProfileSummary";
|
|
import { MissionBriefing } from "@/components/frontend/dashboard/MissionBriefing";
|
|
import { RecentEvents } from "@/components/frontend/dashboard/RecentEvents";
|
|
import { QuickStats } from "@/components/frontend/dashboard/QuickStats";
|
|
import { UpcomingMissions } from "@/components/frontend/dashboard/UpcomingMissions";
|
|
import { ActiveCampaigns } from "@/components/frontend/dashboard/ActiveCampaigns";
|
|
|
|
export default async function HomePage() {
|
|
const payload = await getPayload({ config });
|
|
const headers = await nextHeaders();
|
|
const { user } = await payload.auth({ headers, canSetHeaders: false });
|
|
|
|
if (!user) return null;
|
|
|
|
const userId = user.id;
|
|
|
|
const hasIntelligence = await hasIntelligenceQualification(payload, user).catch(() => false);
|
|
|
|
const [profileRes, experienceRes, assignmentRes] = await Promise.all([
|
|
payload.find({
|
|
collection: "profiles",
|
|
where: { user: { equals: userId } },
|
|
depth: 2,
|
|
select: {
|
|
user: true,
|
|
rank: true,
|
|
progression: { xp: true, awards: true, scoreboard: true, minigames: true },
|
|
createdAt: true,
|
|
},
|
|
}),
|
|
payload.find({
|
|
collection: "experience",
|
|
sort: "requiredPoints",
|
|
limit: 100,
|
|
select: { name: true, requiredPoints: true },
|
|
}),
|
|
payload.find({
|
|
collection: "assignments",
|
|
where: { members: { equals: userId } },
|
|
limit: 10,
|
|
select: { name: true, type: true, callsign: true },
|
|
}),
|
|
]);
|
|
|
|
const profile = profileRes.docs[0];
|
|
const xp = profile?.progression?.xp ?? 0;
|
|
const xpLevel = resolveLevel(xp, experienceRes.docs);
|
|
const profileUser = profile?.user as User | undefined;
|
|
const rank = profile?.rank as Rank | undefined;
|
|
const assignments = assignmentRes.docs as Pick<Assignment, "name" | "type" | "callsign">[];
|
|
|
|
const rankData = rank
|
|
? {
|
|
name: rank.name,
|
|
abbreviation: rank.abbreviation,
|
|
insigniaUrl:
|
|
rank.insignia && typeof rank.insignia === "object" ? (rank.insignia as Media).url : null,
|
|
}
|
|
: { name: "Unknown", abbreviation: "?" };
|
|
|
|
// Fetch upcoming missions (Scheduled or Ready) — up to 5 for the sidebar
|
|
const upcomingRes = await payload
|
|
.find({
|
|
collection: "missions",
|
|
where: {
|
|
"ownershipAndStatus.status": { in: ["Scheduled", "Ready"] },
|
|
},
|
|
sort: "classification.startDateTime",
|
|
limit: 5,
|
|
depth: 2,
|
|
})
|
|
.catch(() => ({ docs: [] }));
|
|
|
|
// If no upcoming, get most recently completed for the briefing card
|
|
let briefingMission = upcomingRes.docs[0] ?? null;
|
|
if (!briefingMission) {
|
|
const completedRes = await payload
|
|
.find({
|
|
collection: "missions",
|
|
where: {
|
|
"ownershipAndStatus.status": { equals: "Completed" },
|
|
},
|
|
sort: "-classification.startDateTime",
|
|
limit: 1,
|
|
depth: 2,
|
|
})
|
|
.catch(() => ({ docs: [] }));
|
|
briefingMission = completedRes.docs[0] ?? null;
|
|
}
|
|
|
|
const mission = briefingMission
|
|
? {
|
|
id: briefingMission.id,
|
|
name: briefingMission.name,
|
|
codeName: briefingMission.codeName,
|
|
summary: briefingMission.summary,
|
|
operationType: briefingMission.operationType,
|
|
classification: {
|
|
missionType: briefingMission.classification.missionType,
|
|
startDateTime: briefingMission.classification.startDateTime,
|
|
estimatedDuration: briefingMission.classification.estimatedDuration,
|
|
map: briefingMission.classification.map as number | Map,
|
|
campaign: briefingMission.classification.campaign as number | Campaign,
|
|
},
|
|
ownershipAndStatus: {
|
|
status: briefingMission.ownershipAndStatus.status,
|
|
},
|
|
missionRoles: briefingMission.missionRoles
|
|
? { maxPlayers: briefingMission.missionRoles.maxPlayers }
|
|
: null,
|
|
coverImage: briefingMission.coverImage,
|
|
}
|
|
: null;
|
|
|
|
const upcomingMissions = upcomingRes.docs.map((m) => ({
|
|
id: m.id,
|
|
name: m.name,
|
|
codeName: m.codeName,
|
|
summary: m.summary,
|
|
operationType: m.operationType,
|
|
classification: {
|
|
missionType: m.classification.missionType,
|
|
startDateTime: m.classification.startDateTime,
|
|
estimatedDuration: m.classification.estimatedDuration,
|
|
map: m.classification.map as number | Map,
|
|
campaign: m.classification.campaign as number | Campaign,
|
|
},
|
|
ownershipAndStatus: {
|
|
status: m.ownershipAndStatus.status,
|
|
},
|
|
missionRoles: m.missionRoles ? { maxPlayers: m.missionRoles.maxPlayers } : null,
|
|
coverImage: m.coverImage,
|
|
}));
|
|
|
|
// Fetch recent events
|
|
const eventsRes = await payload.find({
|
|
collection: "game-event-logs",
|
|
sort: "-timestamp",
|
|
limit: 8,
|
|
depth: 1,
|
|
});
|
|
|
|
const events = eventsRes.docs.map((e) => ({
|
|
id: e.id,
|
|
timestamp: e.timestamp,
|
|
type: e.type,
|
|
message: e.message,
|
|
system: e.system ?? true,
|
|
actor: e.actor as number | User | null | undefined,
|
|
structure: e.structure as number | GameStructure | null | undefined,
|
|
}));
|
|
|
|
// Fetch active campaigns for the sidebar
|
|
const activeCampaignsRes = await payload
|
|
.find({
|
|
collection: "campaigns",
|
|
where: { status: { equals: "inProgress" } },
|
|
limit: 5,
|
|
sort: "-createdAt",
|
|
})
|
|
.catch(() => ({ docs: [] }));
|
|
|
|
const activeCampaigns = activeCampaignsRes.docs.map((c) => ({
|
|
id: c.id,
|
|
name: c.name,
|
|
summary: c.summary,
|
|
status: c.status,
|
|
}));
|
|
|
|
// Fetch counts
|
|
const [campaignsRes, missionsCountRes, structuresRes, factionsRes, profilesRes] =
|
|
await Promise.all([
|
|
payload.find({
|
|
collection: "campaigns",
|
|
where: { status: { equals: "inProgress" } },
|
|
limit: 0,
|
|
}),
|
|
payload
|
|
.find({
|
|
collection: "missions",
|
|
limit: 0,
|
|
})
|
|
.catch(() => ({ totalDocs: 0 })),
|
|
payload.find({
|
|
collection: "game-structures",
|
|
limit: 0,
|
|
}),
|
|
payload.find({
|
|
collection: "factions",
|
|
limit: 0,
|
|
}),
|
|
payload.find({
|
|
collection: "profiles",
|
|
limit: 0,
|
|
}),
|
|
]);
|
|
|
|
return (
|
|
<div className="p-5 flex flex-col gap-6">
|
|
<div className="flex flex-col gap-1">
|
|
<h1 className="text-lg font-semibold">
|
|
Welcome back, {rankData.name}. {/*{profileUser?.displayName ?? user.username}*/}
|
|
</h1>
|
|
<p className="text-sm text-muted-foreground">Here's your operations dashboard.</p>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 xl:grid-cols-3 gap-6">
|
|
{/* ── Main Content (2/3) ──────────────────────────── */}
|
|
<div className="xl:col-span-2 flex flex-col gap-6">
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
|
{profile && profileUser && (
|
|
<ProfileSummary
|
|
displayName={profileUser.displayName}
|
|
username={profileUser.username}
|
|
rank={rankData}
|
|
xpLevel={xpLevel}
|
|
assignments={assignments}
|
|
/>
|
|
)}
|
|
<MissionBriefing mission={mission} />
|
|
</div>
|
|
|
|
<RecentEvents events={events} currentUserId={userId} />
|
|
|
|
<QuickStats
|
|
activeCampaigns={campaignsRes.totalDocs}
|
|
totalMissions={missionsCountRes.totalDocs}
|
|
totalStructures={structuresRes.totalDocs}
|
|
totalFactions={factionsRes.totalDocs}
|
|
totalMembers={profilesRes.totalDocs}
|
|
/>
|
|
</div>
|
|
|
|
{/* ── Right Sidebar (1/3) ─────────────────────────── */}
|
|
<div className="flex flex-col gap-4">
|
|
{hasIntelligence && <UpcomingMissions missions={upcomingMissions} />}
|
|
{hasIntelligence && <ActiveCampaigns campaigns={activeCampaigns} />}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|