feat(missions): wire side-op controls into the mission detail page
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
parent
03f699d852
commit
5577976c96
1 changed files with 142 additions and 23 deletions
|
|
@ -25,6 +25,9 @@ import { MissionRoles } from "@/components/frontend/intelligence/MissionRoles";
|
|||
import { MissionLogistics } from "@/components/frontend/intelligence/MissionLogistics";
|
||||
import { MissionComms } from "@/components/frontend/intelligence/MissionComms";
|
||||
import { MissionAttendance } from "@/components/frontend/intelligence/MissionAttendance";
|
||||
import { SideOpControls } from "@/components/frontend/intelligence/SideOpControls";
|
||||
import { lexicalToText } from "@/lib/intelligence/briefingText";
|
||||
import type { SideOpDialogOption } from "@/components/frontend/intelligence/CreateSideOpDialog";
|
||||
import {
|
||||
type AttendanceResponse,
|
||||
getMissionAttendance,
|
||||
|
|
@ -81,7 +84,7 @@ export default async function MissionDetailPage({ params }: MissionDetailPagePro
|
|||
const map =
|
||||
typeof mission.classification.map === "object" ? (mission.classification.map as Map) : null;
|
||||
const campaign =
|
||||
typeof mission.classification.campaign === "object"
|
||||
typeof mission.classification.campaign === "object" && mission.classification.campaign !== null
|
||||
? (mission.classification.campaign as Campaign)
|
||||
: null;
|
||||
const missionWithCoverImage = mission as MissionWithCoverImage;
|
||||
|
|
@ -96,32 +99,35 @@ export default async function MissionDetailPage({ params }: MissionDetailPagePro
|
|||
const durationMins = mission.classification.estimatedDuration % 60;
|
||||
const durationStr = durationMins > 0 ? `${durationHours}h ${durationMins}m` : `${durationHours}h`;
|
||||
|
||||
// Fetch sibling missions in the same campaign
|
||||
// Fetch sibling missions in the same campaign (missions may have no campaign)
|
||||
const campaignId =
|
||||
typeof mission.classification.campaign === "object"
|
||||
typeof mission.classification.campaign === "object" && mission.classification.campaign !== null
|
||||
? (mission.classification.campaign as Campaign).id
|
||||
: mission.classification.campaign;
|
||||
|
||||
const siblingMissionsRes = await payload
|
||||
.find({
|
||||
collection: "missions",
|
||||
where: {
|
||||
and: [
|
||||
{ "classification.campaign": { equals: campaignId } },
|
||||
{ id: { not_equals: mission.id } },
|
||||
],
|
||||
},
|
||||
sort: "-classification.startDateTime",
|
||||
limit: 5,
|
||||
depth: 0,
|
||||
select: {
|
||||
name: true,
|
||||
codeName: true,
|
||||
ownershipAndStatus: { status: true },
|
||||
classification: { startDateTime: true },
|
||||
},
|
||||
})
|
||||
.catch(() => ({ docs: [] }));
|
||||
const siblingMissionsRes =
|
||||
campaignId != null
|
||||
? await payload
|
||||
.find({
|
||||
collection: "missions",
|
||||
where: {
|
||||
and: [
|
||||
{ "classification.campaign": { equals: campaignId } },
|
||||
{ id: { not_equals: mission.id } },
|
||||
],
|
||||
},
|
||||
sort: "-classification.startDateTime",
|
||||
limit: 5,
|
||||
depth: 0,
|
||||
select: {
|
||||
name: true,
|
||||
codeName: true,
|
||||
ownershipAndStatus: { status: true },
|
||||
classification: { startDateTime: true },
|
||||
},
|
||||
})
|
||||
.catch(() => ({ docs: [] }))
|
||||
: { docs: [] };
|
||||
|
||||
const siblingMissions = siblingMissionsRes.docs;
|
||||
|
||||
|
|
@ -134,6 +140,63 @@ export default async function MissionDetailPage({ params }: MissionDetailPagePro
|
|||
? await hasPermission(payload, currentUser, "missions:update")
|
||||
: false;
|
||||
|
||||
const createdById =
|
||||
typeof mission.createdBy === "object" ? mission.createdBy?.id : mission.createdBy;
|
||||
const isSideOpOwner = currentUser != null && createdById === currentUser.id;
|
||||
const canControlSideOp = canManage || isSideOpOwner;
|
||||
|
||||
let sideOpMaps: SideOpDialogOption[] = [];
|
||||
let sideOpCampaigns: SideOpDialogOption[] = [];
|
||||
let sideOpModLists: SideOpDialogOption[] = [];
|
||||
if (canControlSideOp && currentUser) {
|
||||
const [mapsRes, campaignsRes, modListsRes] = await Promise.all([
|
||||
payload.find({
|
||||
collection: "maps",
|
||||
overrideAccess: true,
|
||||
depth: 0,
|
||||
limit: 200,
|
||||
sort: "name",
|
||||
select: { name: true },
|
||||
}),
|
||||
payload.find({
|
||||
collection: "campaigns",
|
||||
where: { owner: { equals: currentUser.id } },
|
||||
depth: 0,
|
||||
limit: 100,
|
||||
sort: "name",
|
||||
select: { name: true },
|
||||
}),
|
||||
payload.find({
|
||||
collection: "mod-lists",
|
||||
overrideAccess: true,
|
||||
depth: 0,
|
||||
limit: 200,
|
||||
sort: "modlistName",
|
||||
select: { modlistName: true },
|
||||
}),
|
||||
]);
|
||||
sideOpMaps = mapsRes.docs.map((m) => ({ id: m.id, name: m.name }));
|
||||
sideOpCampaigns = campaignsRes.docs.map((c) => ({ id: c.id, name: c.name }));
|
||||
sideOpModLists = modListsRes.docs.map((l) => ({ id: l.id, name: l.modlistName }));
|
||||
const currentCampaignId =
|
||||
typeof mission.classification.campaign === "object" && mission.classification.campaign !== null
|
||||
? mission.classification.campaign.id
|
||||
: mission.classification.campaign;
|
||||
if (
|
||||
currentCampaignId != null &&
|
||||
!sideOpCampaigns.some((c) => c.id === currentCampaignId)
|
||||
) {
|
||||
sideOpCampaigns = [
|
||||
...sideOpCampaigns,
|
||||
{
|
||||
id: currentCampaignId as number,
|
||||
name: campaign ? `${campaign.name} (not yours)` : "Current campaign (not yours)",
|
||||
disabled: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
const authorOrZeusIds = [
|
||||
...(mission.ownershipAndStatus.authors ?? []),
|
||||
...(mission.ownershipAndStatus.zeus ?? []),
|
||||
|
|
@ -334,6 +397,62 @@ export default async function MissionDetailPage({ params }: MissionDetailPagePro
|
|||
|
||||
{/* ── Right Sidebar (1/3) ─────────────────────────── */}
|
||||
<div className="flex flex-col gap-4">
|
||||
{isSideOpOwner && mission.createdBy != null && (
|
||||
<SideOpControls
|
||||
initial={{
|
||||
missionId: mission.id,
|
||||
name: mission.name,
|
||||
codeName: mission.codeName,
|
||||
summary: mission.summary,
|
||||
mapId:
|
||||
typeof mission.classification.map === "object"
|
||||
? mission.classification.map.id
|
||||
: mission.classification.map,
|
||||
missionType: mission.classification.missionType,
|
||||
startDateTime: mission.classification.startDateTime,
|
||||
estimatedDuration: mission.classification.estimatedDuration,
|
||||
maxPlayers: mission.missionRoles?.maxPlayers ?? 32,
|
||||
campaignId:
|
||||
typeof mission.classification.campaign === "object" &&
|
||||
mission.classification.campaign !== null
|
||||
? mission.classification.campaign.id
|
||||
: (mission.classification.campaign ?? null),
|
||||
briefing: (mission.briefing ?? []).map((entry) => ({
|
||||
sectionName: entry.sectionName,
|
||||
details: lexicalToText(entry.details),
|
||||
})),
|
||||
missionObjectives: (mission.missionObjectives ?? []).map((obj) => ({
|
||||
title: obj.title,
|
||||
description: obj.description ?? "",
|
||||
missionCritical: obj.missionCritical ?? true,
|
||||
successOrFailConditions: obj.successOrFailConditions ?? "",
|
||||
redacted: obj.redacted ?? false,
|
||||
})),
|
||||
civilianPresence: mission.civilianPresence ?? "None",
|
||||
radioPlan: lexicalToText(mission.commsAndDocs?.radioPlan ?? null),
|
||||
insertion: mission.logisticsAndSupport?.insertion ?? [],
|
||||
extraction: mission.logisticsAndSupport?.extraction ?? [],
|
||||
assetsAvailable: (mission.logisticsAndSupport?.assetsAvailable ?? []).map(
|
||||
(asset) => ({
|
||||
assetType: asset.assetType,
|
||||
assetCount: asset.assetCount ?? "",
|
||||
doesRespawn: asset.doesRespawn ?? false,
|
||||
assetCallsign: asset.assetCallsign ?? "",
|
||||
}),
|
||||
),
|
||||
requiredModsId:
|
||||
typeof mission.gameDetails.gameSetup?.requiredMods === "object" &&
|
||||
mission.gameDetails.gameSetup?.requiredMods !== null
|
||||
? mission.gameDetails.gameSetup.requiredMods.id
|
||||
: (mission.gameDetails.gameSetup?.requiredMods ?? null),
|
||||
}}
|
||||
status={mission.ownershipAndStatus.status}
|
||||
maps={sideOpMaps}
|
||||
campaigns={sideOpCampaigns}
|
||||
modLists={sideOpModLists}
|
||||
/>
|
||||
)}
|
||||
|
||||
<MissionAttendance
|
||||
missionId={mission.id}
|
||||
currentUserId={currentUser ? (currentUser.id as number) : null}
|
||||
|
|
|
|||
Loading…
Reference in a new issue