diff --git a/frontend/src/pages/CampaignDetailPage.tsx b/frontend/src/pages/CampaignDetailPage.tsx index 41f9af9..6950f78 100644 --- a/frontend/src/pages/CampaignDetailPage.tsx +++ b/frontend/src/pages/CampaignDetailPage.tsx @@ -19,12 +19,14 @@ import { } from "lucide-react"; import { getCampaign, - activateCampaign, completeCampaign, deleteCampaign, removeTestFromCampaign, scheduleCampaign, getCampaignHistory, + submitCampaignForApproval, + approveCampaign, + rejectCampaign, type Campaign, type CampaignHistoryEntry, } from "../api/campaigns"; @@ -68,8 +70,10 @@ export default function CampaignDetailPage() { const [showAddTestModal, setShowAddTestModal] = useState(false); // 0 = hidden, 1 = first confirmation, 2 = ask about tests const [deleteStep, setDeleteStep] = useState<0 | 1 | 2>(0); - // Start-date confirmation modal — shown when campaign has a future start_date - const [startDateWarning, setStartDateWarning] = useState(null); + const [approveModalOpen, setApproveModalOpen] = useState(false); + const [approveStartDate, setApproveStartDate] = useState(""); + const [rejectModalOpen, setRejectModalOpen] = useState(false); + const [rejectReason, setRejectReason] = useState(""); const showToast = (message: string, type: "success" | "error") => { setToast({ message, type }); @@ -79,6 +83,7 @@ export default function CampaignDetailPage() { const role = user?.role ?? ""; const canManage = role === "admin" || role === "red_lead" || role === "blue_lead"; const canComplete = role === "admin" || role === "red_lead"; + const isManager = role === "admin" || role === "manager"; const { data: campaign, @@ -90,33 +95,35 @@ export default function CampaignDetailPage() { enabled: !!campaignId, }); - const activateMutation = useMutation({ - mutationFn: (force: boolean) => activateCampaign(campaignId!, force ? { force: true } : undefined), + const submitMutation = useMutation({ + mutationFn: () => submitCampaignForApproval(campaignId!), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["campaign", campaignId] }); - setStartDateWarning(null); - showToast("Campaign activated", "success"); + showToast("Campaign submitted for approval", "success"); }, - onError: (err: unknown) => { - // The Axios interceptor (client.ts) transforms errors into enhanced Error objects - // with .status (HTTP status), .detail (raw FastAPI detail), and .message (readable string) - type EnhancedError = Error & { - status?: number; - detail?: { code?: string; message?: string } | string; - }; - const e = err as EnhancedError; + onError: (err: Error) => showToast(err.message, "error"), + }); - if (e.status === 409) { - // Future start_date — show confirmation modal using the message from detail - const warningMsg = - typeof e.detail === "object" && e.detail?.message - ? e.detail.message - : e.message; - setStartDateWarning(warningMsg); - } else { - showToast(e.message || "Failed to activate campaign", "error"); - } + const approveMutation = useMutation({ + mutationFn: () => approveCampaign(campaignId!, approveStartDate), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["campaign", campaignId] }); + setApproveModalOpen(false); + setApproveStartDate(""); + showToast("Campaign approved and activated", "success"); }, + onError: (err: Error) => showToast(err.message, "error"), + }); + + const rejectMutation = useMutation({ + mutationFn: () => rejectCampaign(campaignId!, rejectReason), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["campaign", campaignId] }); + setRejectModalOpen(false); + setRejectReason(""); + showToast("Campaign rejected", "success"); + }, + onError: (err: Error) => showToast(err.message, "error"), }); const completeMutation = useMutation({ @@ -262,6 +269,11 @@ export default function CampaignDetailPage() { {campaign.description && ( )} + {campaign.status === "draft" && campaign.rejection_reason && ( +
+ Rejected by manager: {campaign.rejection_reason} +
+ )}
{campaign.threat_actor_name && ( )} + {isManager && campaign.status === "pending_approval" && ( + <> + + + + )} {canComplete && campaign.status === "active" && ( +
+ + + )} + + {/* Reject modal */} + {rejectModalOpen && ( +
+
+

Reject Campaign

+ +