diff --git a/src/app/(frontend)/intelligence/missions/actions.ts b/src/app/(frontend)/intelligence/missions/actions.ts index b7617e7..5c74c9a 100644 --- a/src/app/(frontend)/intelligence/missions/actions.ts +++ b/src/app/(frontend)/intelligence/missions/actions.ts @@ -738,6 +738,70 @@ export async function cancelFridaySideOp( } } +export async function markSideOpReady( + missionId: number, +): Promise> { + try { + const { payload, user } = await authenticate(); + const userId = user.id as number; + + const mission = await payload.findByID({ + collection: "missions", + id: missionId, + depth: 0, + }); + + const canManage = await hasPermission(payload, user, "missions:update"); + if (!canManage && !isSideOpOwner(mission, userId)) { + return { + success: false, + error: "Only the player who scheduled this side operation can mark it Ready.", + }; + } + + const status = mission.ownershipAndStatus.status; + if (status === "Ready") { + return { success: true, data: { missionId: mission.id } }; + } + if (!["Concept", "Planning", "Scheduled"].includes(status)) { + return { + success: false, + error: + status === "Active" + ? "This mission is already underway and cannot be marked Ready." + : `This mission is ${status.toLowerCase()} and can no longer be marked Ready.`, + }; + } + + const updated = await payload.update({ + collection: "missions", + id: missionId, + depth: 0, + overrideAccess: true, + data: { + ownershipAndStatus: { + status: "Ready", + }, + }, + }); + + await emitGameEvent(payload, { + type: EventTypes.MissionReady, + message: `${user.displayName || user.username} marked side operation "${updated.name}" (${updated.codeName}) as Ready — details are locked in and the roll-call can be posted.`, + actor: userId, + targetCollection: "missions", + targetId: updated.id, + }); + + return { success: true, data: { missionId: updated.id } }; + } catch (error) { + const message = error instanceof Error ? error.message : "Unknown error"; + return message === "Unauthorized" + ? { success: false, error: "You must be logged in." } + : { success: false, error: message }; + } +} + export async function moveFridaySideOpToCampaign( missionId: number, campaignId: number, diff --git a/src/components/frontend/intelligence/SideOpControls.tsx b/src/components/frontend/intelligence/SideOpControls.tsx index 8839521..fe9607d 100644 --- a/src/components/frontend/intelligence/SideOpControls.tsx +++ b/src/components/frontend/intelligence/SideOpControls.tsx @@ -3,10 +3,13 @@ import { useState, useTransition } from "react"; import { useRouter } from "next/navigation"; import { toast } from "sonner"; -import { PencilIcon, XCircleIcon } from "lucide-react"; +import { CheckCircleIcon, PencilIcon, XCircleIcon } from "lucide-react"; import { Item, ItemContent, ItemHeader, ItemTitle } from "@/components/ui/item"; import { Button } from "@/components/ui/button"; -import { cancelFridaySideOp } from "@/app/(frontend)/intelligence/missions/actions"; +import { + cancelFridaySideOp, + markSideOpReady, +} from "@/app/(frontend)/intelligence/missions/actions"; import { CreateSideOpDialog, type SideOpDialogInitial, @@ -34,6 +37,19 @@ export function SideOpControls({ const [isPending, startTransition] = useTransition(); const isTerminal = status === "Completed" || status === "Cancelled"; + const canMarkReady = ["Concept", "Planning", "Scheduled"].includes(status); + + function handleMarkReady() { + startTransition(async () => { + const result = await markSideOpReady(initial.missionId); + if (!result.success) { + toast.error(result.error ?? "Failed to mark the side operation as Ready."); + return; + } + toast.success("Side operation marked as Ready. Roll-call can now be posted."); + router.refresh(); + }); + } function handleCancel() { if (!confirmingCancel) { @@ -64,6 +80,12 @@ export function SideOpControls({
+ {canMarkReady && ( + + )}

- Editing covers details, campaign, and narrative. Cancelling frees your scheduling slot - and updates the Discord embed. + Marking the operation Ready locks in the details so the Discord roll-call can be posted + and the mission hooks delivered. Editing covers details, campaign, and narrative. + Cancelling frees your scheduling slot and updates the Discord embed.

diff --git a/src/utils/event-log/eventTypes.ts b/src/utils/event-log/eventTypes.ts index 62588af..facc026 100644 --- a/src/utils/event-log/eventTypes.ts +++ b/src/utils/event-log/eventTypes.ts @@ -126,6 +126,7 @@ export const EventTypes = { MissionAutoComplete: "mission:auto-complete", MissionObjectiveRedactedToggle: "mission:objective-redacted-toggle", MissionSchedule: "mission:schedule", + MissionReady: "mission:ready", MissionUpdate: "mission:update", MissionCancel: "mission:cancel", MissionMoveCampaign: "mission:move-campaign",