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 { MissionLogistics } from "@/components/frontend/intelligence/MissionLogistics";
|
||||||
import { MissionComms } from "@/components/frontend/intelligence/MissionComms";
|
import { MissionComms } from "@/components/frontend/intelligence/MissionComms";
|
||||||
import { MissionAttendance } from "@/components/frontend/intelligence/MissionAttendance";
|
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 {
|
import {
|
||||||
type AttendanceResponse,
|
type AttendanceResponse,
|
||||||
getMissionAttendance,
|
getMissionAttendance,
|
||||||
|
|
@ -81,7 +84,7 @@ export default async function MissionDetailPage({ params }: MissionDetailPagePro
|
||||||
const map =
|
const map =
|
||||||
typeof mission.classification.map === "object" ? (mission.classification.map as Map) : null;
|
typeof mission.classification.map === "object" ? (mission.classification.map as Map) : null;
|
||||||
const campaign =
|
const campaign =
|
||||||
typeof mission.classification.campaign === "object"
|
typeof mission.classification.campaign === "object" && mission.classification.campaign !== null
|
||||||
? (mission.classification.campaign as Campaign)
|
? (mission.classification.campaign as Campaign)
|
||||||
: null;
|
: null;
|
||||||
const missionWithCoverImage = mission as MissionWithCoverImage;
|
const missionWithCoverImage = mission as MissionWithCoverImage;
|
||||||
|
|
@ -96,32 +99,35 @@ export default async function MissionDetailPage({ params }: MissionDetailPagePro
|
||||||
const durationMins = mission.classification.estimatedDuration % 60;
|
const durationMins = mission.classification.estimatedDuration % 60;
|
||||||
const durationStr = durationMins > 0 ? `${durationHours}h ${durationMins}m` : `${durationHours}h`;
|
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 =
|
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 as Campaign).id
|
||||||
: mission.classification.campaign;
|
: mission.classification.campaign;
|
||||||
|
|
||||||
const siblingMissionsRes = await payload
|
const siblingMissionsRes =
|
||||||
.find({
|
campaignId != null
|
||||||
collection: "missions",
|
? await payload
|
||||||
where: {
|
.find({
|
||||||
and: [
|
collection: "missions",
|
||||||
{ "classification.campaign": { equals: campaignId } },
|
where: {
|
||||||
{ id: { not_equals: mission.id } },
|
and: [
|
||||||
],
|
{ "classification.campaign": { equals: campaignId } },
|
||||||
},
|
{ id: { not_equals: mission.id } },
|
||||||
sort: "-classification.startDateTime",
|
],
|
||||||
limit: 5,
|
},
|
||||||
depth: 0,
|
sort: "-classification.startDateTime",
|
||||||
select: {
|
limit: 5,
|
||||||
name: true,
|
depth: 0,
|
||||||
codeName: true,
|
select: {
|
||||||
ownershipAndStatus: { status: true },
|
name: true,
|
||||||
classification: { startDateTime: true },
|
codeName: true,
|
||||||
},
|
ownershipAndStatus: { status: true },
|
||||||
})
|
classification: { startDateTime: true },
|
||||||
.catch(() => ({ docs: [] }));
|
},
|
||||||
|
})
|
||||||
|
.catch(() => ({ docs: [] }))
|
||||||
|
: { docs: [] };
|
||||||
|
|
||||||
const siblingMissions = siblingMissionsRes.docs;
|
const siblingMissions = siblingMissionsRes.docs;
|
||||||
|
|
||||||
|
|
@ -134,6 +140,63 @@ export default async function MissionDetailPage({ params }: MissionDetailPagePro
|
||||||
? await hasPermission(payload, currentUser, "missions:update")
|
? await hasPermission(payload, currentUser, "missions:update")
|
||||||
: false;
|
: 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 = [
|
const authorOrZeusIds = [
|
||||||
...(mission.ownershipAndStatus.authors ?? []),
|
...(mission.ownershipAndStatus.authors ?? []),
|
||||||
...(mission.ownershipAndStatus.zeus ?? []),
|
...(mission.ownershipAndStatus.zeus ?? []),
|
||||||
|
|
@ -334,6 +397,62 @@ export default async function MissionDetailPage({ params }: MissionDetailPagePro
|
||||||
|
|
||||||
{/* ── Right Sidebar (1/3) ─────────────────────────── */}
|
{/* ── Right Sidebar (1/3) ─────────────────────────── */}
|
||||||
<div className="flex flex-col gap-4">
|
<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
|
<MissionAttendance
|
||||||
missionId={mission.id}
|
missionId={mission.id}
|
||||||
currentUserId={currentUser ? (currentUser.id as number) : null}
|
currentUserId={currentUser ? (currentUser.id as number) : null}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue