diff --git a/src/app/(frontend)/intelligence/missions/[id]/page.tsx b/src/app/(frontend)/intelligence/missions/[id]/page.tsx index fc51f6e..c5b108c 100644 --- a/src/app/(frontend)/intelligence/missions/[id]/page.tsx +++ b/src/app/(frontend)/intelligence/missions/[id]/page.tsx @@ -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) ─────────────────────────── */}
+ {isSideOpOwner && mission.createdBy != null && ( + ({ + 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} + /> + )} +