1
0
Fork 0

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:
Jason Fraley 2026-09-06 23:29:54 -04:00
parent 03f699d852
commit 5577976c96

View file

@ -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}